Go back to classic style PHP and use the PDO to call MySQL

This commit is contained in:
Dave Smith-Hayes 2024-11-04 21:13:14 -05:00
parent f788ba63ae
commit d360cceee3
12 changed files with 347 additions and 1476 deletions

3
.editorconfig Normal file
View File

@ -0,0 +1,3 @@
[*.{php,phtml,twig}]
indent_style = space
indent_size = 4

View File

@ -13,10 +13,8 @@
"slim/flash": "^0.4.0", "slim/flash": "^0.4.0",
"odan/session": "^6.1", "odan/session": "^6.1",
"dotenv-org/phpdotenv-vault": "^0.2.4", "dotenv-org/phpdotenv-vault": "^0.2.4",
"react/react": "^1.4",
"robmorgan/phinx": "^0.16.1", "robmorgan/phinx": "^0.16.1",
"react/mysql": "^0.7dev", "ext-pdo": "*"
"react/async": "^4.3"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.1", "phpunit/phpunit": "^11.1",

1500
app/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,6 @@ use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface; use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use React\Mysql\MysqlClient;
use Slim\App; use Slim\App;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Slim\Psr7\Factory\ResponseFactory; use Slim\Psr7\Factory\ResponseFactory;
@ -27,9 +26,9 @@ use Slovocast\Configuration\SiteInformationSchema;
use Slovocast\Domain\Repository\User\UserRepository; use Slovocast\Domain\Repository\User\UserRepository;
use Slovocast\Domain\Repository\User\UserRepositoryInterface; use Slovocast\Domain\Repository\User\UserRepositoryInterface;
use Slovocast\Infrastructure\Api\Database\ConnectionPoolInterface; use Slovocast\Infrastructure\Api\Database\ConnectionPoolInterface;
use Slovocast\Infrastructure\Api\DatabaseConnectionInterface;
use Slovocast\Infrastructure\Api\User\UserAuthorizationInterface; use Slovocast\Infrastructure\Api\User\UserAuthorizationInterface;
use Slovocast\Infrastructure\Database\ConnectionPoolConfig; use Slovocast\Infrastructure\Database\DatabaseConnection;
use Slovocast\Infrastructure\Database\ConnectionPool;
use Slovocast\Infrastructure\User\BasicUserAuthorization; use Slovocast\Infrastructure\User\BasicUserAuthorization;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
@ -128,17 +127,8 @@ class Bootstrap
/** /**
* Database Connections * Database Connections
*/ */
ConnectionPoolInterface::class => function (ContainerInterface $container) { DatabaseConnectionInterface::class => function (ContainerInterface $container) {
$config = $container->get('config')->get('database'); $config = $container->get('config')->get('database');
$pool = new ConnectionPool(new ConnectionPoolConfig(
$config['username'],
$config['password'],
$config['host'],
$config['database']
));
return $pool;
}, },
/** /**
@ -153,7 +143,7 @@ class Bootstrap
*/ */
UserRepositoryInterface::class => function (ContainerInterface $container) { UserRepositoryInterface::class => function (ContainerInterface $container) {
return new UserRepository( return new UserRepository(
$container->get(ConnectionPoolInterface::class), $container->get(DatabaseConnectionInterface::class),
$container->get(UserAuthorizationInterface::class) $container->get(UserAuthorizationInterface::class)
); );
} }

View File

@ -4,14 +4,13 @@ namespace Slovocast\Domain\Repository\User;
use Slovocast\Domain\Entity\User; use Slovocast\Domain\Entity\User;
use Slovocast\Exception\EntityNotFoundException; use Slovocast\Exception\EntityNotFoundException;
use Slovocast\Infrastructure\Api\Database\ConnectionPoolInterface; use Slovocast\Infrastructure\Api\Database\DatabaseConnectionInterface;
use Slovocast\Infrastructure\Api\User\UserAuthorizationInterface; use Slovocast\Infrastructure\Api\User\UserAuthorizationInterface;
use function React\Async\await;
class UserRepository implements UserRepositoryInterface class UserRepository implements UserRepositoryInterface
{ {
public function __construct( public function __construct(
private ConnectionPoolInterface $connectionPool, private DatabaseConnectionInterface $db,
private UserAuthorizationInterface $userAuth private UserAuthorizationInterface $userAuth
) {} ) {}
@ -37,13 +36,13 @@ class UserRepository implements UserRepositoryInterface
*/ */
public function get(int $id): User public function get(int $id): User
{ {
$query = "SELECT * FROM users WHERE id = ? LIMIT 1"; $query = "SELECT * FROM users WHERE id = :id LIMIT 1";
/** @var $conn ConnectionPool */ $statement = $this->db->getConnection()->prepare($query);
$conn = $this->connectionPool->getConnection(); $statement->execute([ ':id' => $id ]);
$results = await($conn->query($query, [ $id ])); $results = $statement->fetch(\PDO::FETCH_ASSOC);
return $this->userFromQueryResults($results->resultRows[0]); return $this->userFromQueryResults($results);
} }
/** /**
@ -54,46 +53,47 @@ class UserRepository implements UserRepositoryInterface
*/ */
public function getFromEmail(string $email): User public function getFromEmail(string $email): User
{ {
$query = "SELECT * FROM users WHERE email = ? LIMIT 1"; $query = "SELECT * FROM users WHERE email = :email LIMIT 1";
$results = await($this->db->query($query, [ $email ])); $statement = $this->db->getConnection()->prepare($query);
$statement->execute([ ':email' => $email ]);
$results = $statement->fetch(\PDO::FETCH_ASSOC);
if (!count($results->resultRows)) { if (!count($results)) {
throw new EntityNotFoundException("Unable to find User"); throw new EntityNotFoundException("Unable to find User");
} }
return $this->userFromQueryResults($results->resultRows[0]); return $this->userFromQueryResults($results);
} }
public function create(User $user): bool public function create(User $user): bool
{ {
$query = "INSERT INTO users (email, password, name) $query = "INSERT INTO users (email, password, name)
VALUES (?, ?, ?)"; VALUES (:email, :password, :name)";
$results = await($this->db->query($query, [ $results = $this->db->getConnection()->exec($query, [
$user->getEmail(), ':email' => $user->getEmail(),
$this->userAuth->hash($user->getPassword()), ':password' => $this->userAuth->hash($user->getPassword()),
$user->getName(), ':name' => $user->getName(),
])); ]);
return (bool) $results->insertId; return (bool) $results;
} }
public function update(User $user): bool public function update(User $user): bool
{ {
$query = "UPDATE users $query = "UPDATE users
SET email = ?, SET email = :email,
name = ?, name = :name,
password = ? password = :password
WHERE id = ?"; WHERE id = :id";
$results = await($this->db->query($query, [ $statement = this->db->prepare($query);
return $statement->execute([
$user->getEmail(), $user->getEmail(),
$user->getName(), $user->getName(),
$this->userAuth->hash($user->getPassword()), $this->userAuth->hash($user->getPassword()),
$user->getId() $user->getId()
])); ]);
return (bool) $results->affectedRows;
} }
public function verifyPassword(string $email, string $password): bool public function verifyPassword(string $email, string $password): bool

View File

@ -1,47 +0,0 @@
<?php
namespace Slovocast\Infrastructure\Api\Database;
use Exception;
interface ConnectionPoolInterface
{
public function getTotalIdleConnections(): int;
public function getTotalActiveConnections(): int;
/**
* If there is at least one idle connection waiting to be used.
*
* @return bool
*/
public function hasIdleConnection(): bool;
/**
* Sets the new wait timeout for acquiring a connection.
*
* @param int $ms The amount of time in seconds.
*/
public function setWaitTimeout(int $s): void;
public function getWaitTimeout(): int;
public function getConnectionLimit(): int;
/**
* This method will grab a connection from the Idle list, push it into the Active list and returns that same
* connection to the caller. If there is no active connections, the method will wait based on the `waitTimeout`
* value (in seconds) and throw an exception when the timeout has been reached.
*
* @throws Exception When the Wait Timeout is surpassed waiting for an idle connection
* @return PooledConnectionInterface
*/
public function getConnection(): PooledConnectionInterface;
/**
* This method will return a connection from the Active list into the Idle list.
*
* @param PooledConnectionInterface $connection
* @return void
*/
public function releaseConnection(PooledConnectionInterface $connection): void;
}

View File

@ -0,0 +1,11 @@
<?php
namespace Slovocast\Infrastructure\Api\Database;
use PDO;
interface DatabaseConnectionInterface
{
public function getConnection(): PDO;
public function setNewConnection(PDO $connection): void;
}

View File

@ -1,8 +0,0 @@
<?php
namespace Slovocast\Infrastructure\Api\Database;
interface PooledConnectionInterface
{
public function release(): void;
}

View File

@ -1,89 +0,0 @@
<?php
namespace Slovocast\Infrastructure\Database;
use Slovocast\Infrastructure\Api\Database\ConnectionPoolInterface;
use Slovocast\Infrastructure\Api\Database\PooledConnectionInterface;
use SplObjectStorage;
class ConnectionPool implements ConnectionPoolInterface
{
private int $waitTimeout;
private SplObjectStorage $idleConnections;
private SplObjectStorage $activeConnections;
private ConnectionPoolConfig $config;
public function __construct(ConnectionPoolConfig $config)
{
$this->idleConnections = new SplObjectStorage();
$this->activeConnections = new SplObjectStorage();
for ($i = 0; $i < $this->config->getTotalConnections(); $i++) {
$pooledConnection = new PooledConnection($this->config->getDsnString());
$this->idleConnections->attach($$pooledConnection);
}
$this->waitTimeout = $config->getPoolWaitTimeout();
}
public function getTotalIdleConnections(): int
{
return $this->idleConnections->count();
}
public function getTotalActiveConnections(): int
{
return $this->activeConnections->count();
}
public function hasIdleConnection(): bool
{
return $this->idleConnections->count() > 0;
}
public function setWaitTimeout(int $s): void
{
$this->waitTimeout = $s;
}
public function getWaitTimeout(): int
{
return $this->waitTimeout;
}
public function getConnectionLimit(): int
{
return $this->config->getTotalConnections();
}
/**
* @TODO Throw an exception when a total timeout is exceeded. We do not want
* to get into an infinite loop.
*
* @return PooledConnectionInterface
*/
public function getConnection(): PooledConnectionInterface
{
if (!$this->hasIdleConnection()) {
\React\Async\delay((float) $this->getWaitTimeout());
return $this->getConnection();
}
// Remove from the idle pool
$conn = $this->idleConnections->current();
$this->idleConnections->detach($conn);
// Attach to the pool to the connectin, add it to the active pool
$conn->setConnectionPool($this);
$this->activeConnections->attach($conn);
return $conn;
}
public function releaseConnection(PooledConnectionInterface $connection): void
{
if ($this->activeConnections->contains($connection)) {
$this->activeConnections->detach($connection);
$this->idleConnections->attach($connection);
}
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace Slovocast\Infrastructure\Database;
class ConnectionPoolConfig
{
const DEFAULT_WAIT_TIMEOUT = 60;
const DEFAULT_TOTAL_CONNECTIONS = 10;
public function __construct(
public readonly string $username,
public readonly string $password,
public readonly string $database,
public readonly string $host,
protected int $port = 3306,
protected int $poolWaitTimeout = self::DEFAULT_WAIT_TIMEOUT,
protected int $totalConnections = self::DEFAULT_TOTAL_CONNECTIONS
) { }
public function getPort(): int
{
return $this->port;
}
public function getPoolWaitTimeout(): int
{
return $this->poolWaitTimeout;
}
public function getTotalConnections(): int
{
return $this->totalConnections;
}
public function getDsnString(): string
{
return sprintf(
"%s:%s@%s/%s",
rawurlencode($this->username),
rawurlencode($this->password),
$this->host,
$this->database
);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Slovocast\Infrastructure\Database;
use Slovocast\Infrastructure\Api\Database\DatabaseConnectionInterface;
use PDO;
class DatabaseConnection implements DatabaseConnectionInterface
{
public function __construct(private PDO $pdo) {}
public function getConnection(): PDO
{
return $this->pdo;
}
public function setNewConnection(PDO $pdo): void
{
$this->pdo = $pdo;
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace Slovocast\Infrastructure\Database;
use React\Mysql\MysqlClient;
use Slovocast\Infrastructure\Api\Database\ConnectionPoolInterface;
use Slovocast\Infrastructure\Api\Database\PooledConnectionInterface;
class PooledConnection extends MysqlClient implements PooledConnectionInterface
{
protected ConnectionPoolInterface $connectionPool;
public function setConnectionPool(ConnectionPoolInterface $connectionPool): self
{
$this->connectionPool = $connectionPool;
return $this;
}
public function release(): void
{
$this->connectionPool->releaseConnection($this);
}
}