Porównujemy naszą klasę PDOConnection oraz klasę Database z projektu PHPiggy, którego autor w dość ciekawy sposób podszedł do tematu. Zaczynajmny!

Jednym z istotnych elementów naszej klasy jest parser pliku ini:

class ConfigINI {

    public static $iniPath = __DIR__ . "/config.ini";

    public static function getPDOCredentials(){
        $ini_array = parse_ini_file(static::$iniPath, true);
        return $ini_array['DB_CREDENTIALS'];
    }
}

Nasza klasa zawiera connection, requiredKeys oraz metodę getConnection:


class PDOConnection {

    protected $connection;
    

    public static $requiredKeys = [
        'driver',
        'host',
        'db_name',
        'db_user',
        'db_password',
        'port'
    ];

    //(...)

    public function getConnection(): PDO|null
    {
        return $this->connection;
    }

    //(...)

}

Metoda hasRequiredKeys sprawdza, czy odpowiednie klucze zostały przekazane:

class PDOConnection {

    protected $connection;
    

    public static $requiredKeys = [
        'driver',
        'host',
        'db_name',
        'db_user',
        'db_password',
        'port'
    ];

    //(...)

    public static function hasRequiredKeys($credentials){
        foreach(self::$requiredKeys as $key){
            if(!array_key_exists($key, $credentials))
                return false;
        }
        return true;
    }
}

Metoda parseCredentials parsuje dane na dsn, użytkownika i hasło:

class PDOConnection {

    protected $connection;
    

    public static $requiredKeys = [
        'driver',
        'host',
        'db_name',
        'db_user',
        'db_password',
        'port'
    ];

    //(...)

    public static function parseCredentials($credentials){
        $driver = $credentials['driver'];
        $config = http_build_query(arg_separator:';', data: 
            [
                'host' => $credentials['host'],
                'port' => $credentials['port'],
                'dbname' => $credentials['db_name']
            ]
            );

        $dsn = "{$driver}:{$config}";
       
        return [$dsn, $credentials['db_user'], $credentials['db_password']];
    }

}

Metoda connect pobiera dane z pliku ini, dodaje port 3306, jeżeli go nie określiliśmy, sprawdza klucze, parsuje dsn i inne głupoty i nawiązuje połączenie:

class PDOConnection {

    protected $connection;
    
    //(...)

    public function connect(): PDOConnection{
        try {
            $this->credentials = ConfigINI::getPDOCredentials();

            if(!array_key_exists('port', $this->credentials))
                $this->credentials['port'] = 3306;

            if(!static::hasRequiredKeys($this->credentials))
                throw new Error("Credentials dont have valid keys");

            $parsed = static::parseCredentials($this->credentials);
            $this->connection = new PDO(...$parsed);

        } catch (Exception $e) {
            echo $e->getMessage();
        }
        
        return $this;
    }

}

Autor PHPiggy proponuje coś innego – rozdzielenie na PDOConnection i PDOStatement:

use PDO, PDOException, PDOStatement;

class Database
{
  private PDO $connection;
  private PDOStatement $stmt;
  //(...)
}

W podobny sposób nawiązuje połączenie:

use PDO, PDOException, PDOStatement;

class Database
{
  private PDO $connection;
  private PDOStatement $stmt;

  public function __construct(
    string $driver,
    array $config,
    string $username,
    string $password
  ) {
    $config = http_build_query(data: $config, arg_separator: ';');

    $dsn = "{$driver}:{$config}";

    try {
      $this->connection = new PDO($dsn, $username, $password, [
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
      ]);
    } catch (PDOException $e) {
      die("Unable to connect to database");
    }
  }
}

Zapewnia bardzo elastyczne metody:

class Database
{
  private PDO $connection;
  private PDOStatement $stmt;

  //(...)

   public function query(string $query, array $params = []): Database
  {
    $this->stmt = $this->connection->prepare($query);

    $this->stmt->execute($params);

    return $this;
  }

  public function count()
  {
    return $this->stmt->fetchColumn();
  }
  
  //(...)

}

Takie postawienie sprawy pozwala mu na bardzo elastyczny kod:

class UserService
{
  public function __construct(private Database $db)
  {
  }

  public function isEmailTaken(string $email)
  {
    //DOBRZE NAPISANA METODA QUERY Z BINDINGIEM TABLICOWYM JAKO DRUGIM ARGUMENTEM

    $emailCount = $this->db->query(
      "SELECT COUNT(*) FROM users WHERE email = :email",
      [
        'email' => $email
      ]
    )->count();

    //METODA QUERY ZWRACA THIS A NA THIS MOŻNA CHAINOWAĆ INNE METODY, NP. COUNT
    //DZIĘKI SEPARACJI NA CONNECTION I STATEMENT TO WSZYSTKO DZIAŁA, 
    //BO QUERY ZAPISAŁO STMT I ZWRÓCIŁO THIS, ZAŚ COUNT OPERUJE NA STMT

  //(...)
}

Całość klasy wygląda tak i stanowi bardzo dobry wzór:

class Database
{
  private PDO $connection;
  private PDOStatement $stmt;

  //(...)

   public function query(string $query, array $params = []): Database
  {
    $this->stmt = $this->connection->prepare($query);

    $this->stmt->execute($params);

    return $this;
  }

  public function count()
  {
    return $this->stmt->fetchColumn();
  }
  
 public function find()
  {
    return $this->stmt->fetch();
  }

  public function id()
  {
    return $this->connection->lastInsertId();
  }

  public function findAll()
  {
    return $this->stmt->fetchAll();
  }

}