W pracy programisty pełnego stosu bardzo satysfakcjonujące jest to (przynajmniej dla mnie), że jakiego zadania się człowiek nie podejmie, musi zawsze patrzeć na system z którym pracuje w szerszej, pełnej perspektywie. Jest to zadanie trudne i bardzo odpowiedzialne. Na co dzień pracuję tworząc nie tylko kod (core) aplikacji, ale także przygotowuję zapytania dla baz danych czy poprawiam UX aplikacji. I chociaż dla mnie wszystkie te elementy tworzą jeden spójny system, to muszę być przygotowany na to, że do kodu przysiądzie osoba, która będzie chciała skupić się wyłącznie na części systemu, a pozostałe będzie traktowała jako abstrakcję, o której nie musi wiedzieć nic oprócz tego, co dostarcza.

Dlatego ważne jest, aby już na samym początku przygotowywania rozwiązania dobrze odseparować od siebie warstwy logiczne. Dzisiaj chciałbym pokazać pomysł i jego prostą implementację, w jaki sposób można odseparować od siebie warstwę aplikacji od dostępu do bazy danych - na przykładzie prostego systemu ORM (ang. Object-relational mapping). Warto zaznaczyć, że korzystając z gotowych frameworków z reguły mamy już dostęp do tego typu narzędzi, dlatego nie ma sensu wynajdować koła na nowo i korzystać z gotowców. Jednak czasami warto spróbować napisać coś samodzielnie, aby przekonać się jak tego typu biblioteki działają i czy są trudne do zrealizowania samodzielnie :).‍

Na początek stwórzmy w bazie danych (na potrzeby tego wpisu użyję bazy MySQL) tabelę post. Następnie utwórzmy w niej pola: id CHAR(5), title VARCHAR(100), author VARCHAR(200), status INT. Do tabeli dodajmy kilka wpisów, np. zapytaniem:

INSERT INTO post (id, title, author, status) VALUES ('10000', 'Hello', 'Bartlomiej Romanek', 1); INSERT INTO post (id, title, author, status) VALUES ('20000', 'World', 'Janusz Tracz', 2); INSERT INTO post (id, title, author, status) VALUES ('30000', 'Friends', 'Bartłomiej Romanek', 2);

Utwórzmy także nowy projekt w naszym ulubionym IDE i dodajmy do niego katalog ORM, a w nim folder Model oraz Repository. W katalogu Model będziemy przechowywać encje (odzwzorowanie wiersza z bazy danych na obiekt w PHP) - otwórzmy więc tutaj nową klasę o nazwie Post, a w niej zdefiniujmy właściwości (pola z publicznym modyfikatorem dostępu) o nazwach tak jak w bazie danych:

<?php class Post { public $id; public $title; public $author; public $status; }

Następnie tworzymy repozytorium dla postów:

<?php require 'ORM/Connection.php'; require 'ORM/Model/Post.php'; class PostRepository { private $connection; private $model; public function __construct() { $this->connection = new Connection('root', '', 'example', 'utf8'); $this->model = new Post(); } public function get(string $id = ''): array { if ($id === '') { $posts = $this->connection->query("SELECT * FROM post", []); } else { $posts = $this->connection->query("SELECT * FROM post WHERE id = :id", [':id' => $id]); } $posts = $this->connection->query("SELECT * FROM post WHERE {$where}", [':id' => $id]); $return = []; foreach ($posts as $post) { $i = 0; $object = clone($this->model); foreach ($object as &$field) { if (isset($post[$i])) { $field = $post[$i]; } $i++; } $return[] = $object; } return $return; } }

Tak przygotowane repozytorium posiada tylko jedną metodę - get - która pozwala na pobieranie tablicy postów z bazy danych. To, jakie posty zostaną pobrane z bazy, zależy od id, które przekażemy do metody. Jeżeli nie przekażemy żadnego id, to zostaną zwrócone wszystkie elementy z tabeli. Powyższa metoda ma proste działanie – najpierw zostają pobrane z bazy rekordy, następnie w pętli foreach wypełniana jest instancja encji i zapisywana do tablicy, która na końcu zostaje zwrócona.

Na koniec dodajemy brakującą klasę Connection. Klasa ta jest wrapperem na klasę mysqli pozwalająca na połączenie z bazą danych mysql:

<?php class Connection { private static $db; public function __construct($login, $password, $name, $charset) { if(self::$db === null) { self::$db = new \mysqli('localhost', $login, $password, $name); if (!self::$db->connect_error) { self::$db->set_charset($charset); } } } public function query(string $query, array $params = []): array { foreach ($params as $param => $value) { $query = str_ireplace($param, '?', $query); } $statement = self::$db->prepare($query); foreach ($params as $param => $value) { switch (true) { case is_int($value): $type = 'i'; break; case is_double($value): $type = 'd'; break; case is_string($value): default: $type = 's'; break; } $statement->bind_param($type, $value); } $statement->execute(); $result = $statement->get_result(); $result = $result->fetch_all(MYSQLI_NUM); return $result; } }

Powyższa metoda zakłada, że zapytanie do bindowania ma składnię PDO (a więc znaki zapytania w miejscach, gdzie znajdują się zmienne). Dlatego też występuje w metodzie zamiana parametrów na znaki zapytania.

I to już w sumie wszystko! Możemy teraz utworzyć w głównym katalogu plik index.php, w nim wykorzystać nasze repozytorium:

<?php require 'ORM/Repository/PostRepository.php'; $postRepository = new PostRepository(); $posts = $postRepository->get('10000'); $post = $posts[0]; echo $post->author;

Wynikiem jest napis:

Bartlomiej Romanek

Oczywiście powyższy przykład jest uproszczony do bólu ;). Ale mam nadzieję, że pokazał ideę. Powyższe pliki źródłowe w całości możesz pobrać stąd.