W naszym projekcie MVC zrobiliśmy model z prawdziwego zdarzenia, działający jak w Laravelu, z modelami statycznymi oraz metodą save i kreatywnym wykorzystaniem magicznych metod get i set. Teraz poznamy wzorzec Repository.

Nie jest on aż tak przydatny, ale warto znać. Najpierw jednak połączenie z bazą danych:

$host = '127.0.0.1';
$db   = 'mydb12345';
$user = 'root';
$pass = 'pass';
$port = "3306";
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset;port=$port";

$pdo = new PDO($dsn, $user, $pass);

$q = $pdo->query('SELECT COUNT(*) FROM PEOPLE');
print_r($q->fetch());

//Array ( [COUNT(*)] => 14 [0] => 14 )

Ok, teraz przypominam, jak wyglądał nasz PersonModel:

class PersonModel extends Model {

    protected static $tablename = 'people';
    protected static $keys = ['name', 'age'];
 
}

Klasa bazowa Model robiła za nas wszystko, toż to prawie ORM ta klasa. Jedyne co ustawialiśmy to nazwy tabeli i kolumn.

Cóż, tutaj nie będzie aż tak różowo:

class PersonEntity {
    public function __construct(public $id, public $name,  public $age)
    {
        
    }
}

Teraz Repository:

class PersonRepository {
    public function __construct(private PDO $pdo) {}

    private function arrayToModel(array $entry): PersonEntity {
        return new PersonEntity(
            $entry['ID'],
            $entry['name'],
            $entry['age']
        );
    }
}

Metoda arrayToModel robi za nas wszystko. Dalej już tylko nasza umiejętność korzystania z PDO:

class PersonRepository {

    public function __construct(private PDO $pdo) {}

    //(...)

    public function fetchById(int $id): ?PersonEntity {
        $stmt = $this->pdo->prepare('SELECT * FROM `people` WHERE `ID` = :id');

        $stmt->bindValue(':id', $id);
        $stmt->execute();
        $entry = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!empty($entry)) 
            return $this->arrayToModel($entry);

        return null;
    }

$repo = new PersonRepository($pdo);
$jim = $repo->fetchById(14);

print_r($jim);
//PersonEntity Object ( [id] => 14 [name] => Jim [age] => 29 )

Teraz fetch:

class PersonRepository {

    public function __construct(private PDO $pdo) {}

    //(...)

    public function fetch(): array {
        $stmt = $this->pdo->prepare('SELECT * 
            FROM `people` ');

        $stmt->execute();

        $models = [];
        $entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
        foreach($entries AS $entry) {
            $models[] = $this->arrayToModel($entry);
        }

        return $models;
    }
}

$repo = new PersonRepository($pdo);
$jim = $repo->fetchById(14);
print_r($jim);
//PersonEntity Object ( [id] => 14 [name] => Jim [age] => 29 )

$models = $repo->fetch();
print_r($models[13]);
//PersonEntity Object ( [id] => 14 [name] => Jim [age] => 29 )

Jako że przepis na paginację znamy, paginate również nie powinno nas dziwić:

class PersonRepository {

    public function __construct(private PDO $pdo) {}

    //(...)

    public function paginate(int $page, int $perPage = 5): array {

        $page = max(1, $page);

        $stmt = $this->pdo->prepare('SELECT * 
            FROM `people` 
            ORDER BY `ID` ASC
            LIMIT :limit OFFSET :offset');

        $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
        $stmt->bindValue(':offset', ($page - 1) * $perPage, PDO::PARAM_INT);

        $stmt->execute();

        $models = [];
        $entries = $stmt->fetchAll(PDO::FETCH_ASSOC);

        foreach($entries AS $entry) {
            $models[] = $this->arrayToModel($entry);
        }

        return $models;
    }
}

//(...)

$paginated_models = $repo->paginate(2, 3);
print_r($paginated_models);
// Array ( 
//     [0] => PersonEntity Object ( [id] => 4 [name] => Yolo [age] => 22 ) 
//     [1] => PersonEntity Object ( [id] => 5 [name] => Abc [age] => 22 ) 
//     [2] => PersonEntity Object ( [id] => 6 [name] => Jane [age] => 22 ) 
//     )

Count to łatwizna:

//(...)

public function count(): int {
        $stmt = $this->pdo->prepare('SELECT COUNT(*) AS `count` FROM `people`');
        $stmt->execute();
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result['count'];
    }
}

//(...)

echo $repo->count();
// 14

Teraz update:

class PersonRepository {

    public function __construct(private PDO $pdo) {}

    //(...)

    public function update(int $id, array $properties): PersonEntity {
        $stmt = $this->pdo->prepare('UPDATE `people` 
            SET 
                `name` = :name,
                `age` = :age
            WHERE `id` = :id');

        $stmt->bindValue(':id', $id);
        $stmt->bindValue(':name', $properties['name']);
        $stmt->bindValue(':age', $properties['age']);
      
        $stmt->execute();

        return $this->fetchById($id);
    }
}

//(...)

$yolo_man = $repo->update(4, ['name' => 'YoloMan', 'age' => 35]);
print_r($yolo_man);
//PersonEntity Object ( [id] => 4 [name] => YoloMan [age] => 35 )

Jedyny problem, jaki tu widzę (dostosowałem wzorzec do naszych potrzeb bazując na przykładzie z internetu) to paginacja:

$repo = new PersonRepository($pdo);

$paginated_models = $repo->paginate(20, 3);
print_r($paginated_models);
// Array ()

Znaczy – można przekazać stronę większą niż ostatnia strona. Naprawmy to:

class PersonRepository {
    public function __construct(private PDO $pdo) {}

    //(...)

    public function count(): int {
        $stmt = $this->pdo->prepare('SELECT COUNT(*) AS `count` FROM `people`');
        $stmt->execute();
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result['count'];
    }

    public function lastPage($perPage): int {
        $count = $this->count();
        $lastPage = ceil($count / $perPage);
        return $lastPage;
    }
    
}

//(...)

$repo = new PersonRepository($pdo);

echo $repo->lastPage(3);
//5
print_r($repo->paginate(5, 3));
// Array ( 
// [0] => PersonEntity Object ( [id] => 13 [name] => Tim [age] => 22 ) 
// [1] => PersonEntity Object ( [id] => 14 [name] => Jim [age] => 29 ) 
// ) 
print_r($repo->paginate(6, 3));
// Array ()

Teraz możemy zastosować zabezpieczenie w paginate:

class PersonRepository {
    public function __construct(private PDO $pdo) {}

    //(...)

    public function paginate(int $page, int $perPage = 5): array {

        $page = max(1, $page);
        $lastPage = $this->lastPage($perPage);
        $page = min($page, $lastPage);

        $stmt = $this->pdo->prepare('SELECT * 
            FROM `people` 
            ORDER BY `ID` ASC
            LIMIT :limit OFFSET :offset');

        $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
        $stmt->bindValue(':offset', ($page - 1) * $perPage, PDO::PARAM_INT);

        $stmt->execute();

        $models = [];
        $entries = $stmt->fetchAll(PDO::FETCH_ASSOC);

        foreach($entries AS $entry) {
            $models[] = $this->arrayToModel($entry);
        }

        return $models;
    }

   
}

//(...)

$repo = new PersonRepository($pdo);

echo $repo->lastPage(3);
//5
print_r($repo->paginate(5, 3));
// Array ( 
// [0] => PersonEntity Object ( [id] => 13 [name] => Tim [age] => 22 ) 
// [1] => PersonEntity Object ( [id] => 14 [name] => Jim [age] => 29 ) 
// ) 
print_r($repo->paginate(6, 3));
// Array ( 
// [0] => PersonEntity Object ( [id] => 13 [name] => Tim [age] => 22 ) 
// [1] => PersonEntity Object ( [id] => 14 [name] => Jim [age] => 29 ) 
// )