Why bother with repositories - php

I feel a bit stupid asking this question since there are a lot of resources talking and explaining mappers and repositories but I can't seem to get my head around it. So I've created some example code to explain my confusion. Please note that I don't know if this code would actually work I wrote this as an example.
This would be the entity / class (Quote.php)
class Quote {
private $id;
private $author;
private $content;
public function getId() {
return $this->id;
}
public function getAuthor() {
return $this->author;
}
public function getContent() {
return $this->content;
}
public function setId(int $id) {
$this->id = $id;
}
public function getAuthor(string $author) {
$this->author = $author;
}
public function setContent(string $content) {
$this->content = $content;
}
}
And this would be the mapper (QuoteMapper.php)
class QuoteMapper {
private $PDO;
public function __construct(PDO $PDO) {
$this->PDO = $PDO;
}
public function find(int $id = null, string $search = null) {
if (!empty($id) && !empty($search)) {
//Search for id and search word
$stmt = $this->PDO->prepare("SELECT `id`, `author`, `content` FROM `quotes` WHERE `id` = :id AND `content` LIKE :search LIMIT 1");
$stmt->bindParam('search', $search, PDO::PARAM_INT);
$stmt->bindParam('id', $id, PDO::PARAM_INT);
else if (!empty($id)) {
//search for id only
$stmt = $this->PDO->prepare("SELECT `id`, `author`, `content` FROM `quotes` WHERE `id` = :id AND `content` LIKE :search LIMIT 1");
$stmt->bindParam('id', $id, PDO::PARAM_INT);
} else if (!empty($search)) {
//search for search word only
$stmt = $this->PDO->prepare("SELECT `id`, `author`, `content` FROM `quotes` WHERE `id` = :id AND `content` LIKE :search LIMIT 1");
$stmt->bindParam('search', $search, PDO::PARAM_INT);
}
$stmt->execute();
$stmt->bindColumn('id', $id);
$stmt->bindColumn('author', $author);
$stmt->bindColumn('content', $content);
$stmt->fetch();
$quote = new Image();
$quote->setId($title);
$quote->setAuthor($source);
$quote->setContent($alternative);
return $image;
}
public function save(Quote $quote) {
//A save function
}
public function delete(Quote $quote) {
//A delete function
}
}
Last but not least, this would be the repository (QuoteRepository.php)
class ArticleRepository {
private $articleMapper;
public function __construct(ArticleMapper $articleMapper) {
$this->articleMapper = $articleMapper;
}
public function find(int $id = null, string $search = null) {
$article = $this->articleMapper->find($id, $search);
return $article;
}
public function save(Quote $quote) {
$this->articleMapper->save($user);
}
public function delete(Quote $quote) {
$this->articleMapper->delete($user);
}
}
As I understand my mapper isn't 'wrong' since the purpose of the mapper is to do things such as get and set data from persistent data storage (such as MySQL)
A Data Mapper is a Data Access Layer that performs bidirectional
transfer of data between a persistent data store (often a relational
database) and an in-memory data representation (the domain layer).
From Wikipedia
But my repository doesn't actually do anything. It just passes the function call along to the mapper? So I can only assume that my mapper contains code that should be in the repository, but what code would that be? Or perhaps I've completely misunderstood how data mappers and repositories would work together.
If there are any other things that I have done that are wrong or considered bad practice I would like to hear it. I'm really trying to figure this out! :)

DataMapper is a layer to isolate an application from a concrete database. It transforms an object into a record of a database and a record into an object. DataMapper gives us the ability to work with Database and be unaware of what RDBMS we use. Example:
interface DataMapperInterface
{
/**
* Find objects by a criteria
*
* #param array $criteria Search params
* #return Quote[] Found entities
*/
public function find(array $criteria);
/**
* Insert an object into a database
*
* #param Quote $object Object that will be inserted
*/
public function insert(Quote $object);
/**
* Update an object date in a database
*
* #param Quote $object Object that will be updated
*/
public function update(Quote $object);
/**
* Remove an object from a database
*
* #param Quote $object Object that will be removed
*/
public function delete(Quote $object);
}
Repository is a layer for encapsulation of logic of a query building. It gives us the ability to work with a collection of objects and be unaware to work with Database anything.
class Repository
{
/**
* #var DataMapperInterface Mapper to transform objects
*/
protected $mapper;
/**
* Constructor
*
* #param DataMapperInterface $mapper Mapper to transform objects
*/
public function __construct(DataMapperInterface $mapper)
{
$this->mapper = $mapper;
}
/**
* Find all objects
*
* #return Quote[] Found entities
*/
public function findAll()
{
return $this->mapper->find([]);
}
/**
* Find an object by an identifier
*
* #return Quote[] Found entities
*/
public function findById(integer $id)
{
$criteria = ['id' => $id];
return $this->mapper->find($criteria);
}
/**
* Find objects by an author name
*
* #return Quote[] Found entities
*/
public function findByAuthor($name)
{
$criteria = ['author' => $name];
return $this->mapper->find($criteria);
}
/**
* Save an object into the repository
*/
public function save(Quote $object)
{
if (empty($object->id)) {
$this->mapper->insert($object);
} else {
$this->mapper->update($object);
}
a }
/**
* Remove an object from the repository
*/
public function remove(Quote $object)
{
$this->mapper->delete($object);
}
}
This is the very simple instance and all is more difficult in a real application: queries are bigger, repository can cooperate with many other patterns (Query Object to query building, Unit of Work to track changes, Identity Map to avoid repeatedly-load of objects, etc.)

Related

PHP/MVC/PDO - beginTransaction outside of Database class

could someone help me on this? I have following classes (all functional, abbreviated here for sake of legibility):
class Database {
private $host = DB_HOST;
// etc...
public function __construct() {
$dsn = 'mysql:host=' . $this->host . ';dbname=' . $this->dbname;
$options = array(PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION);
try {
$this->dbh = new PDO($dsn, $this->user, $this->pass, $options);
} catch (PDOException $e) {
$this->error = $e->getMessage();
echo $this->error;
}
}
public function beginTransaction() {
$this->stmt = $this->dbh->beginTransaction();
}
and a class for let’s say books;
class Books extends Controller {
public function __construct() {
$this->model = $this->loadModel('BookModel');
}
// etc.
$this->model->beginTransaction();
and the BookModel looks like:
class BookModel {
protected $db;
public function __construct() {
$this->db = new Database;
}
public function beginTransaction() {
$this->db->beginTransaction();
}
I know I can only access the PDO beginTransaction inside of the Database class, but is there another way, or I have to use this complicated path, call the method that calls the method that calls the PDO method?
I have a feeling I’m doing something very stupid here. Maybe extending the BookModel to the Database class, but that doesn’t feel right either.
Thanks!
Some suggestions:
[a] You shouldn't create objects (with "new") inside class methods. Instead you should inject existent instances into constructors/setters. This is named dependency injection and can be applied with a dependency injection container.
Dependency Injection and Dependency Inversion in PHP - James Mallison - PHPTour 2017 Nantes
PHP-DI The dependency injection container for humans
[b] As #YourCommonSense noted, Database would greatly benefit from a single PDO instance, injected in the constructor. The injection task would be the job of the DI container. For example, if you'd use PHP-DI, there would be a definition entry for creating a database connection:
return [
'database-connection' => function (ContainerInterface $container) {
$parameters = $container->get('database.connection');
$dsn = $parameters['dsn'];
$username = $parameters['username'];
$password = $parameters['password'];
$connectionOptions = [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$connection = new PDO($dsn, $username, $password, $connectionOptions);
return $connection;
},
];
and another definition entry to inject it in Database:
return [
Database::class => autowire()
->constructorParameter('connection', get('database-connection')),
];
The Database contructor would look like:
public function __construct(PDO $connection) {
$this->dbh = $connection;
}
[c] The model is not a class (like BookModel). It is a layer (model layer, or domain model), composed of multiple components: entities (or domain objects), value objects, data mappers, repositories, domain services. Your BookModel is a combination btw. an entity and a data mapper (at least). Note: inheriting it from Database is wrong, because a model can't be a database.
How should a model be structured in MVC?
The difference between domains, domain models, object models and domain objects
[d] You shouldn't inject models into controllers. Instead, controllers should use so-called application services (also named use cases, or actions, or interactors). These services contain the so-called application logic and are the proper way to decouple the presentation layer (or delivery mechanism) - which, among other components, includes the controllers and the views - from the domain model. The application services also assure the communication btw. the two layers. Note: there could also be domain services, specific to the domain and separated from the application services, which are specific to the application.
Sandro Mancuso : Crafted Design
Ruby Midwest 2011 - Keynote: Architecture the Lost Years by Robert Martin
Robert "Uncle Bob" Martin - Architecture: The Lost Years
How should a model be structured in MVC?
[e] Database class is not needed at all! You already have the very elegant & powerful PDO at disposal, to handle the database operations.
[f] Actually, it is not wrong to "call the method that calls the method that calls the PDO method". Each method in this chain encapsulates a certain behavior, specific to the current object. Though, the functionality of each method should add some plus value. Otherwise, it wouldn't make sense to have this chain, indeed. An example: In an application service, you can directly use a data mapper to fetch a book by id from the database:
class FindBooksService {
public function __construct(
private BookMapper $bookMapper
) {
}
public function findBookById(?int $id = null): ?Book {
return $this->bookMapper->fetchBookById($id);
}
}
class BookMapper {
public function __construct(
private PDO $connection
) {
}
public function fetchBookById(?int $id): ?Book {
$sql = 'SELECT * FROM books WHERE id = :id LIMIT 1';
// Fetch book data from database; convert the record to a Book object ($book).
//...
return $book;
}
}
Now, you could use a repository instead, to hide even the fact that the queried data comes from a database. This makes sense, since a repository object is seen as a collection of objects of a certain type (here Book) by other components. Therefore, the other components think that the repository is a collection of books, not a bunch of data in some database, and they ask the repository for them correspondingly. The repository will, in turn, interogate the data mapper to query the database. So, the previous code becomes:
class FindBooksService {
/**
* #param BookCollection $bookCollection The repository: a collection of books, e.g. of Book instances.
*/
public function __construct(
private BookCollection $bookCollection
) {
}
public function findBookById(?int $id = null): ?Book {
return $this->bookCollection->findBookById($id);
}
}
class BookCollection {
private array $books = [];
public function __construct(
private BookMapper $bookMapper
) {
}
/**
* This method adds a plus value to the omolog method in the data mapper (fetchBookById):
* - caches the Book instances in the $books list, therefore reducing the database querying operations;
* - hides the fact, that the data comes from a database, from the external world, e.g. other components.
* - provides an elegant collection-like interface.
*/
public function findBookById(?int $id): ?Book {
if (!array_key_exists($id, $this->books)) {
$book = $this->bookMapper->fetchBookById($id);
$this->books[id] = $book;
}
return $this->books[$id];
}
}
class BookMapper {
// the same...
}
[g] A "real" mistake would be to pass an object through other objects, just to be used by the last object.
Alternative example code:
I wrote some code as an alternative to yours. I hope it will help you better understand, how the components of an MVC-based application could work together.
Important: Notice the namespace SampleMvc/Domain/Model/: that's the domain model. Note that the application services, e.g. all components from SampleMvc/App/Service/, should communicate ONLY with the domain model components, e.g. with the components from SampleMvc/Domain/Model/ (mostly interfaces), not from SampleMvc/Domain/Infrastructure/. In turn, the DI container of your choice will take care of injecting the proper class implementations from SampleMvc/Domain/Infrastructure/ for the interfaces of SampleMvc/Domain/Model/ used by the application services.
Notice the method updateBook() in SampleMvc/Domain/Infrastructure/Book/PdoBookMapper.php. I included a transaction code in it, along with two great links. Have fun.
Project structure:
SampleMvc/App/Controller/Book/AddBook.php:
<?php
namespace SampleMvc\App\Controller\Book;
use Psr\Http\Message\{
ResponseInterface,
ServerRequestInterface,
};
use SampleMvc\App\Service\Book\{
AddBook as AddBookService,
Exception\BookAlreadyExists,
};
use SampleMvc\App\View\Book\AddBook as AddBookView;
/**
* A controller for adding a book.
*
* Let's assume the existence of this route definition:
*
* $routeCollection->post('/books/add', SampleMvc\App\Controller\Book\AddBook::class);
*/
class AddBook {
/**
* #param AddBookView $view The view for presenting the response to the request back to the user.
* #param AddBookService $addBookService An application service for adding a book to the model layer.
*/
public function __construct(
private AddBookView $view,
private AddBookService $addBookService
) {
}
/**
* Add a book.
*
* The book details are submitted from a form, using the HTTP method "POST".
*
* #param ServerRequestInterface $request A server request.
* #return ResponseInterface The response to the current request.
*/
public function __invoke(ServerRequestInterface $request): ResponseInterface {
$authorName = $request->getParsedBody()['authorName'];
$title = $request->getParsedBody()['title'];
try {
$book = $this->addBookService($authorName, $title);
$this->view->setBook($book);
} catch (BookAlreadyExists $exception) {
$this->view->setErrorMessage(
$exception->getMessage()
);
}
$response = $this->view->addBook();
return $response;
}
}
SampleMvc/App/Controller/Book/FindBooks.php:
<?php
namespace SampleMvc\App\Controller\Book;
use Psr\Http\Message\ResponseInterface;
use SampleMvc\App\View\Book\FindBooks as FindBooksView;
use SampleMvc\App\Service\Book\FindBooks as FindBooksService;
/**
* A controller for finding books.
*
* Let's assume the existence of this route definition:
*
* $routeCollection->post('/books/find/{authorName}', [SampleMvc\App\Controller\FindBooks::class, 'findBooksByAuthorName']);
*/
class FindBooks {
/**
* #param FindBooksView $view The view for presenting the response to the request back to the user.
* #param FindBooksService $findBooksService An application service for finding books by querying the model layer.
*/
public function __construct(
private FindBooksView $view,
private FindBooksService $findBooksService
) {
}
/**
* Find books by author name.
*
* The author name is provided by clicking on a link of some author name
* in the browser. The author name is therefore sent using the HTTP method
* "GET" and passed as argument to this method by a route dispatcher.
*
* #param string|null $authorName (optional) An author name.
* #return ResponseInterface The response to the current request.
*/
public function findBooksByAuthorName(?string $authorName = null): ResponseInterface {
$books = $this->findBooksService->findBooksByAuthorName($authorName);
$response = $this->view
->setBooks($books)
->findBooksByAuthorName()
;
return $response;
}
}
SampleMvc/App/Service/Book/Exception/BookAlreadyExists.php:
<?php
namespace SampleMvc\App\Service\Book\Exception;
/**
* An exception thrown if a book already exists.
*/
class BookAlreadyExists extends \OverflowException {
}
SampleMvc/App/Service/Book/AddBook.php:
<?php
namespace SampleMvc\App\Service\Book;
use SampleMvc\Domain\Model\Book\{
Book,
BookMapper,
};
use SampleMvc\App\Service\Book\Exception\BookAlreadyExists;
/**
* An application service for adding a book.
*/
class AddBook {
/**
* #param BookMapper $bookMapper A data mapper for transfering books
* to and from a persistence system.
*/
public function __construct(
private BookMapper $bookMapper
) {
}
/**
* Add a book.
*
* #param string|null $authorName An author name.
* #param string|null $title A title.
* #return Book The added book.
*/
public function __invoke(?string $authorName, ?string $title): Book {
$book = $this->createBook($authorName, $title);
return $this->storeBook($book);
}
/**
* Create a book.
*
* #param string|null $authorName An author name.
* #param string|null $title A title.
* #return Book The newly created book.
*/
private function createBook(?string $authorName, ?string $title): Book {
return new Book($authorName, $title);
}
/**
* Store a book.
*
* #param Book $book A book.
* #return Book The stored book.
* #throws BookAlreadyExists The book already exists.
*/
private function storeBook(Book $book): Book {
if ($this->bookMapper->bookExists($book)) {
throw new BookAlreadyExists(
'A book with the author name "' . $book->getAuthorName() . '" '
. 'and the title "' . $book->getTitle() . '" already exists'
);
}
return $this->bookMapper->saveBook($book);
}
}
SampleMvc/App/Service/Book/FindBooks.php:
<?php
namespace SampleMvc\App\Service\Book;
use SampleMvc\Domain\Model\Book\{
Book,
BookMapper,
};
/**
* An application service for finding books.
*/
class FindBooks {
/**
* #param BookMapper $bookMapper A data mapper for transfering books
* to and from a persistence system.
*/
public function __construct(
private BookMapper $bookMapper
) {
}
/**
* Find a book by id.
*
* #param int|null $id (optional) A book id.
* #return Book|null The found book, or null if no book was found.
*/
public function findBookById(?int $id = null): ?Book {
return $this->bookMapper->fetchBookById($id);
}
/**
* Find books by author name.
*
* #param string|null $authorName (optional) An author name.
* #return Book[] The found books list.
*/
public function findBooksByAuthorName(?string $authorName = null): array {
return $this->bookMapper->fetchBooksByAuthorName($authorName);
}
}
SampleMvc/App/View/Book/AddBook.php:
<?php
namespace SampleMvc\App\View\Book;
use SampleMvc\{
App\View\View,
Domain\Model\Book\Book,
};
use Psr\Http\Message\ResponseInterface;
/**
* A view for adding a book.
*/
class AddBook extends View {
/** #var Book The added book. */
private Book $book = null;
/**
* Add a book.
*
* #return ResponseInterface The response to the current request.
*/
public function addBook(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Book/AddBook.html.twig', [
'activeNavItem' => 'AddBook',
'book' => $this->book,
'error' => $this->errorMessage,
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Set the book.
*
* #param Book $book A book.
* #return static
*/
public function setBook(Book $book): static {
$this->book = $book;
return $this;
}
}
SampleMvc/App/View/Book/FindBooks.php:
<?php
namespace SampleMvc\App\View\Book;
use SampleMvc\{
App\View\View,
Domain\Model\Book\Book,
};
use Psr\Http\Message\ResponseInterface;
/**
* A view for finding books.
*/
class FindBooks extends View {
/** #var Book[] The list of found books. */
private array $books = [];
/**
* Find books by author name.
*
* #return ResponseInterface The response to the current request.
*/
public function findBooksByAuthorName(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Book/FindBooks.html.twig', [
'activeNavItem' => 'FindBooks',
'books' => $this->books,
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Set the books list.
*
* #param Book[] $books A list of books.
* #return static
*/
public function setBooks(array $books): static {
$this->books = $books;
return $this;
}
}
SampleMvc/App/View/View.php:
<?php
namespace SampleMvc\App\View;
use Psr\Http\Message\ResponseFactoryInterface;
use SampleLib\Template\Renderer\TemplateRendererInterface;
/**
* View.
*/
abstract class View {
/** #var string The error message */
protected string $errorMessage = '';
/**
* #param ResponseFactoryInterface $responseFactory Response factory.
* #param TemplateRendererInterface $templateRenderer Template renderer.
*/
public function __construct(
protected ResponseFactoryInterface $responseFactory,
protected TemplateRendererInterface $templateRenderer
) {
}
/**
* Set the error message.
*
* #param string $errorMessage An error message.
* #return static
*/
public function setErrorMessage(string $errorMessage): static {
$this->errorMessage = $errorMessage;
return $this;
}
}
SampleMvc/Domain/Infrastructure/Book/PdoBookMapper.php:
<?php
namespace SampleMvc\Domain\Infrastructure\Book;
use SampleMvc\Domain\Model\Book\{
Book,
BookMapper,
};
use PDO;
/**
* A data mapper for transfering Book entities to and from a database.
*
* This class uses a PDO instance as database connection.
*/
class PdoBookMapper implements BookMapper {
/**
* #param PDO $connection Database connection.
*/
public function __construct(
private PDO $connection
) {
}
/**
* #inheritDoc
*/
public function bookExists(Book $book): bool {
$sql = 'SELECT COUNT(*) as cnt FROM books WHERE author_name = :author_name AND title = :title';
$statement = $this->connection->prepare($sql);
$statement->execute([
':author_name' => $book->getAuthorName(),
':title' => $book->getTitle(),
]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
return ($data['cnt'] > 0) ? true : false;
}
/**
* #inheritDoc
*/
public function saveBook(Book $book): Book {
if (isset($book->getId())) {
return $this->updateBook($book);
}
return $this->insertBook($book);
}
/**
* #inheritDoc
*/
public function fetchBookById(?int $id): ?Book {
$sql = 'SELECT * FROM books WHERE id = :id LIMIT 1';
$statement = $this->connection->prepare($sql);
$statement->execute([
'id' => $id,
]);
$record = $statement->fetch(PDO::FETCH_ASSOC);
return ($record === false) ?
null :
$this->convertRecordToBook($record)
;
}
/**
* #inheritDoc
*/
public function fetchBooksByAuthorName(?string $authorName): array {
$sql = 'SELECT * FROM books WHERE author_name = :author_name';
$statement = $this->connection->prepare($sql);
$statement->execute([
'author_name' => $authorName,
]);
$recordset = $statement->fetchAll(PDO::FETCH_ASSOC);
return $this->convertRecordsetToBooksList($recordset);
}
/**
* Update a book.
*
* This method uses transactions as example.
*
* Note: I never worked with transactions, but I
* think the code in this method is not wrong.
*
* #link https://phpdelusions.net/pdo#transactions (The only proper) PDO tutorial: Transactions
* #link https://phpdelusions.net/pdo (The only proper) PDO tutorial
* #link https://phpdelusions.net/articles/error_reporting PHP error reporting
*
* #param Book $book A book.
* #return Book The updated book.
* #throws \Exception Transaction failed.
*/
private function updateBook(Book $book): Book {
$sql = 'UPDATE books SET author_name = :author_name, title = :title WHERE id = :id';
try {
$this->connection->beginTransaction();
$statement = $this->connection->prepare($sql);
$statement->execute([
':author_name' => $book->getAuthorName(),
':title' => $book->getTitle(),
':id' => $book->getId(),
]);
$this->connection->commit();
} catch (\Exception $exception) {
$this->connection->rollBack();
throw $exception;
}
return $book;
}
/**
* Insert a book.
*
* #param Book $book A book.
* #return Book The newly inserted book.
*/
private function insertBook(Book $book): Book {
$sql = 'INSERT INTO books (author_name, title) VALUES (:author_name, :title)';
$statement = $this->connection->prepare($sql);
$statement->execute([
':author_name' => $book->getAuthorName(),
':title' => $book->getTitle(),
]);
$book->setId(
$this->connection->lastInsertId()
);
return $book;
}
/**
* Convert the given record to a Book instance.
*
* #param array $record The record to be converted.
* #return Book A Book instance.
*/
private function convertRecordToBook(array $record): Book {
$id = $record['id'];
$authorName = $record['author_name'];
$title = $record['title'];
$book = new Book($authorName, $title);
$book->setId($id);
return $book;
}
/**
* Convert the given recordset to a list of Book instances.
*
* #param array $recordset The recordset to be converted.
* #return Book[] A list of Book instances.
*/
private function convertRecordsetToBooksList(array $recordset): array {
$books = [];
foreach ($recordset as $record) {
$books[] = $this->convertRecordToBook($record);
}
return $books;
}
}
SampleMvc/Domain/Model/Book/Book.php:
<?php
namespace SampleMvc\Domain\Model\Book;
/**
* Book entity.
*/
class Book {
/**
* #param string|null $authorName (optional) The name of an author.
* #param string|null $title (optional) A title.
*/
public function __construct(
private ?string $authorName = null,
private ?string $title = null
) {
}
/**
* Get id.
*
* #return int|null
*/
public function getId(): ?int {
return $this->id;
}
/**
* Set id.
*
* #param int|null $id An id.
* #return static
*/
public function setId(?int $id): static {
$this->id = $id;
return $this;
}
/**
* Get the author name.
*
* #return string|null
*/
public function getAuthorName(): ?string {
return $this->authorName;
}
/**
* Set the author name.
*
* #param string|null $authorName The name of an author.
* #return static
*/
public function setAuthorName(?string $authorName): static {
$this->authorName = $authorName;
return $this;
}
/**
* Get the title.
*
* #return string|null
*/
public function getTitle(): ?string {
return $this->title;
}
/**
* Set the title.
*
* #param string|null $title A title.
* #return static
*/
public function setTitle(?string $title): static {
$this->title = $title;
return $this;
}
}
SampleMvc/Domain/Model/Book/BookMapper.php:
<?php
namespace SampleMvc\Domain\Model\Book;
use SampleMvc\Domain\Model\Book\Book;
/**
* An interface for various data mappers used to
* transfer Book entities to and from a persistence system.
*/
interface BookMapper {
/**
* Check if a book exists.
*
* #param Book $book A book.
* #return bool True if the book exists, false otherwise.
*/
public function bookExists(Book $book): bool;
/**
* Save a book.
*
* #param Book $book A book.
* #return Book The saved book.
*/
public function saveBook(Book $book): Book;
/**
* Fetch a book by id.
*
* #param int|null $id A book id.
* #return Book|null The found book, or null if no book was found.
*/
public function fetchBookById(?int $id): ?Book;
/**
* Fetch books by author name.
*
* #param string|null $authorName An author name.
* #return Book[] The found books list.
*/
public function fetchBooksByAuthorName(?string $authorName): array;
}

setFetchMode() seems not to be working and not returning an object?

I'm currently working with one of my friends on making a portfolio for all of his projects and as strangely as it seems, I cannot manage to make setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'Class Namespace') working on his code while it is working on my projects.
Here is the error returned by PHP :
!(https://cdn.discordapp.com/attachments/525023910698156034/583938068311179265/unknown.png)
Here is the class Entity which contains the __construct() function and the hydration() function :
<?php
namespace App\Entity;
class Entity {
public function __construct(array $array) {
$this->hydrate($array);
}
public function hydrate($array) {
foreach ($array as $key => $value) {
$setter = 'set' . ucfirst($key);
if (method_exists($this, $setter)) {
$this->$setter($value);
}
}
}
}
Then, here is the Project Class which implements the class Entity :
<?php
namespace App\Entity;
class Project extends Entity implements \JsonSerializable {
private $_title;
private $_description;
private $_imagePath;
private $_link;
private $_repoLink;
private $_creationDate;
public function jsonSerialize() {
return [
'title' => $this->_title,
'description' => $this->_description,
'imagePath' => $this->_imagePath,
'link' => $this->_link,
'repoLink' => $this->_repoLink,
'creationDate' => $this->_creationDate,
];
}
/
* #return string
*/
public function getTitle(): string {
return $this->_title;
}
/
* #param string $title
*/
public function setTitle(string $title) {
$this->_title = $title;
}
/
* #return string
*/
public function getDescription(): string {
return $this->_description;
}
/
* #param string $description
*/
public function setDescription(string $description) {
$this->_description = $description;
}
/
* #return string
*/
public function getImagePath(): string {
return $this->_imagePath;
}
/
* #param string $imagePath
*/
public function setImagePath(string $imagePath) {
$this->_imagePath = $imagePath;
}
/
* #return string
*/
public function getLink(): string {
return $this->_link;
}
/
* #param string $link
*/
public function setLink(string $link) {
$this->_link = $link;
}
/
* #return string
*/
public function getRepoLink(): string {
return $this->_repoLink;
}
/
* #param string $repoLink
*/
public function setRepoLink(string $repoLink) {
$this->_repoLink = $repoLink;
}
/
* #return \DateTime
*/
public function getCreationDate(): \DateTime {
return $this->_creationDate;
}
/
* #param string $creationDate
*/
public function setCreationDate(string $creationDate) {
$this->_creationDate = new \DateTime($creationDate);
}
}
And finally, here is the SQL request :
<?php
namespace App\Model;
class ProjectManager extends Manager {
/**
* return a collection of Project objects
* #return Project[]
* #throws \Exception
*/
public function getProjects() {
$db = $this->getDb();
$q = $db->query(
'SELECT id,
title,
description,
image_path AS imagePath,
link,
repo_link AS repoLink,
creation_date AS creationDate
FROM my_website_projects
ORDER BY creationDate'
);
$q->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, 'App\Entity\Project');
$projects = $q->fetchAll();
return $projects;
}
}
The only thing that seems to work is to add PDO::FETCH_ASSOC in the fetchAll() but then it doesn't return an object but an array....
Your help would be much appreciated on this problem ! :)
As far as I know, there is no solution to this problem. There is no fetch mode that creates an object passing the returned row as a constructor parameter.
So I would change the code to this a bit clumsy but working solution
public function getProjects() {
$db = $this->getDb();
$q = $db->query(
'SELECT id,
title,
description,
image_path AS imagePath,
link,
repo_link AS repoLink,
creation_date AS creationDate
FROM my_website_projects
ORDER BY creationDate'
);
$projects = [];
while($row = $q->fetch(PDO::FETCH_ASSOC)) {
$projects[] = new App\Entity\Project($row);
}
return $projects;
}
Your mistake is that you slightly confused the way PDO creates objects.
This way is rather blunt, PDO just takes an object and fills it properties, the process is not much different from filling an associative array.
So now you can tell how did your other code work:
first, the constructor parameter in your other class is optional, it means PHP won't complain for it.
second, in your other class properties are spelled equal to column names in the database, and PDO happily fills them as described above.
So, as another solution you can fix these 2 issues: make the constructor parameter optional and remove the underscore from the property names.
Some time ago I wrote an article on fetching obejects with PDO, you may find it useful.

Is this the common structure for the domain mapper model?

Hopefully i am asking this on the right stack exchange forum. If not please do let me know and I will ask somewhere else. I have also asked on Code Review, but the community seems a lot less active.
As I have self learned PHP and all programming in general, I have only recently found out about 'Data Mappers' which allows data to be passed into classes without said classes knowing where the data comes from. I have read some of the positives of using mappers and why they make it 'easier' to perform upgrades later down the line, however I am really struggling to find out the reccomended way of using mappers and their layouts in a directory structure.
Let's assume we have a simple application whos purpose is to echo out a first name and last name of a user.
The way I have been using/creating mappers (as well as the file structure is as follows):
index.php
include 'classes/usermapper.php';
include 'classes/user.php';
$user = new User;
$userMapper = new userMapper;
try {
$user->setData([
$userMapper->fetchData([
'username'=>'peter1'
])
]);
} catch (Exception $e) {
die('Error occurred');
}
if ($user->hasData()) {
echo $user->fullName();
}
classes/user.php
class User {
private $_data;
public function __construct() { }
public function setData($userObject = null) {
if (!$userObject) { throw new InvalidArgumentException('No Data Set'); }
$this->_data = $dataObject;
}
public function hasData() {
return (!$this->_data) ? false : true;
}
public function fullName() {
return ucwords($this->_data->firstname.' '.$this->_data->lastname);
}
}
classes/usermapper.php
class userMapper {
private $_db;
public function __construct() { $this->_db = DB::getInstance(); }
public function fetchData($where = null) {
if (!is_array($where)) {
throw new InvalidArgumentException('Invalid Params Supplied');
}
$toFill = null;
foreach($where as $argument=>$value) {
$toFill .= $argument.' = '.$value AND ;
}
$query = sprintf("SELECT * FROM `users` WHERE %s ", substr(rtrim($toFill), 0, -3));
$result = $this->_db->query($query); //assume this is just a call to a database which returns the results of the query
return $result;
}
}
With understanding that the users table contains a username, first name and last name, and also that a lot of sanitizing checks are missing, why are mappers convenient to use?
This is a very long winded way in getting data, and assuming that users aren't everything, but instead orders, payments, tickets, companies and more all have their corresponding mappers, it seems a waste not to create just one mapper and implement it everywhere in each class.
This allows the folder structure to look a whole lot nicer and also means that code isn't repeated as often.
The example mappers looks the same in every case bar the table the data is being pulled from.
Therefore my question is. Is this how data mappers under the 'domain model mappers' should look like, and if not how could my code be improved? Secondly is this model needed in all cases of needing to pull data from a database, regardless of the size of class, or should this model only be used where the user.php class in this case is very large?
Thank you in advance for all help.
The Data Mapper completely separates the domain objects from the persistent storage (database) and provides methods that are specific to domain-level operations. Use it to transfer data from the domain to the database and vice versa. Within a method, a database query is usually executed and the result is then mapped (hydrated) to a domain object or a list of domain objects.
Example:
The base class: Mapper.php
abstract class Mapper
{
protected $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
}
The file: BookMapper.php
class BookMapper extends Mapper
{
public function findAll(): array
{
$sql = "SELECT id, title, price, book_category_id FROM books;";
$statement = $this->db->query($sql);
$items = [];
while ($row = $statement->fetch()) {
$items[] = new BookEntity($row);
}
return $items;
}
public function findByBookCategoryId(int $bookCategoryId): array
{
$sql = "SELECT id, title, price, book_category_id
FROM books
WHERE book_category_id = :book_category_id;";
$statement = $this->db->prepare($sql);
$statement->execute(["book_category_id" => $bookCategoryId]);
$items = [];
while ($row = $statement->fetch()) {
$items[] = new BookEntity($row);
}
return $items;
}
/**
* Get one Book by its ID
*
* #param int $bookId The ID of the book
* #return BookEntity The book
* #throws RuntimeException
*/
public function getById(int $bookId): BookEntity
{
$sql = "SELECT id, title, price, book_category_id FROM books
WHERE id = :id;";
$statement = $this->db->prepare($sql);
if (!$result = $statement->execute(["id" => $bookId])) {
throw new DomainException(sprintf('Book-ID not found: %s', $bookId));
}
return new BookEntity($statement->fetch());
}
public function insert(BookEntity $book): int
{
$sql = "INSERT INTO books SET title=:title, price=:price, book_category_id=:book_category_id";
$statement = $this->db->prepare($sql);
$result = $statement->execute([
'title' => $book->getTitle(),
'price' => $book->getPrice(),
'book_category_id' => $book->getBookCategoryId(),
]);
if (!$result) {
throw new RuntimeException('Could not save record');
}
return (int)$this->db->lastInsertId();
}
}
The file: BookEntity.php
class BookEntity
{
/** #var int|null */
protected $id;
/** #var string|null */
protected $title;
/** #var float|null */
protected $price;
/** #var int|null */
protected $bookCategoryId;
/**
* Accept an array of data matching properties of this class
* and create the class
*
* #param array|null $data The data to use to create
*/
public function __construct(array $data = null)
{
// Hydration (manually)
if (isset($data['id'])) {
$this->setId($data['id']);
}
if (isset($data['title'])) {
$this->setTitle($data['title']);
}
if (isset($data['price'])) {
$this->setPrice($data['price']);
}
if (isset($data['book_category_id'])) {
$this->setBookCategoryId($data['book_category_id']);
}
}
/**
* Get Id.
*
* #return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* Set Id.
*
* #param int|null $id
* #return void
*/
public function setId(?int $id): void
{
$this->id = $id;
}
/**
* Get Title.
*
* #return null|string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* Set Title.
*
* #param null|string $title
* #return void
*/
public function setTitle(?string $title): void
{
$this->title = $title;
}
/**
* Get Price.
*
* #return float|null
*/
public function getPrice(): ?float
{
return $this->price;
}
/**
* Set Price.
*
* #param float|null $price
* #return void
*/
public function setPrice(?float $price): void
{
$this->price = $price;
}
/**
* Get BookCategoryId.
*
* #return int|null
*/
public function getBookCategoryId(): ?int
{
return $this->bookCategoryId;
}
/**
* Set BookCategoryId.
*
* #param int|null $bookCategoryId
* #return void
*/
public function setBookCategoryId(?int $bookCategoryId): void
{
$this->bookCategoryId = $bookCategoryId;
}
}
The file: BookCategoryEntity.php
class BookCategoryEntity
{
const FANTASY = 1;
const ADVENTURE = 2;
const COMEDY = 3;
// here you can add the setter and getter methods
}
The table structure: schema.sql
CREATE TABLE `books` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`price` decimal(19,2) DEFAULT NULL,
`book_category_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `book_category_id` (`book_category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE `book_categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*Data for the table `book_categories` */
insert into `book_categories`(`id`,`title`) values (1,'Fantasy');
insert into `book_categories`(`id`,`title`) values (2,'Adventure');
insert into `book_categories`(`id`,`title`) values (3,'Comedy');
Usage
// Create the database connection
$host = '127.0.0.1';
$dbname = 'test';
$username = 'root';
$password = '';
$charset = 'utf8';
$collate = 'utf8_unicode_ci';
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES $charset COLLATE $collate"
];
$db = new PDO($dsn, $username, $password, $options);
// Create the data mapper instance
$bookMapper = new BookMapper($db);
// Create a new book entity
$book = new BookEntity();
$book->setTitle('Harry Potter');
$book->setPrice(29.99);
$book->setBookCategoryId(BookCategoryEntity::FANTASY);
// Insert the book entity
$bookId = $bookMapper->insert($book);
// Get the saved book
$newBook = $bookMapper->getById($bookId);
var_dump($newBook);
// Find all fantasy books
$fantasyBooks = $bookMapper->findByBookCategoryId(BookCategoryEntity::FANTASY);
var_dump($fantasyBooks);

Function Dependancy PHP

So in php im trying to get a public variable in the database class that connects to a database when the class is created. Like so -
<?php
class database {
public $_link;
public function __construct (){
$this->_link = new PDO("mysql:host=localhost; dbname=swinkidc_student", "swinkidc_student", "");
}
}
and...
<?php
class user{
private $db;
public function __construct() {
$this->db = new database;
}
/**
* Returns the ID of a user.
* #param string $user
* #return mixed
*/
public function getUserID($user){
$query = $_link->prepare("SELECT `user_id` FROM `users` WHERE `username` = :user");
$query->bindParam(":user", $user);
$query->execute();
$result = $query->fetch(PDO::FETCH_ASSOC);
return $result['user_id'];
}
/**
* Checks if given user is active.
* #param string $user
* #return bool Returns true/false if user is active.
*/
public function isUserActive($user){
}
}
If I extened database in user I can obviously refer to _link if i make it private and it will work, however, I don't think that i should inherit just for getting something like that..
Whenever i try the about i get :
Fatal error: Call to a member function prepare() on a non-object in /home/swinkidc/public_html/studentreach/core/authentication/user.php on line 18
As said, if i try the inheritance way it will work, I just don't feel like thats a great way of doing something like this.
Any suggestions please?
Thanks!
link is actually a property of database and not a property of user. Therefore use:
$query = $this->db->_link->...
You are just missing the $this->db-> before:
$query = $this->db->_link->prepare("SELECT `user_id` FROM `users` WHERE `username` = :user");
Now, it should work; you're trying to access it directly. First access the Database-link holding object through the $this->db and only then access the property $_link.
If you need $_link as a variable because it's shorter, assign the object $this->db->_link to $_link:
$_link = $this->db->_link;
At the beginning of your getUserID function, then it'll work too.
Try, in the User class, something as:
<?php
class user{
private $db;
public function __construct() {
$this->db = new database;
}
/**
* Returns the ID of a user.
* #param string $user
* #return mixed
*/
public function getUserID($user){
$query = $this->db->_link->prepare("SELECT `user_id` FROM `users` WHERE `username` = :user");
$query->bindParam(":user", $user);
$query->execute();
$result = $query->fetch(PDO::FETCH_ASSOC);
return $result['user_id'];
}
/**
* Checks if given user is active.
* #param string $user
* #return bool Returns true/false if user is active.
*/
public function isUserActive($user){
}
}
This can works as _link is not a attribute of User, but of database, and, if you extend the class database, it's acessible from the methods in link (at least in Java, i not know if the same way works for PHP), but not if you just instantiate it. It's just basic OOP.
Good luck.
Better to invert dependencies and wrap $db.
class database {
private $_link;
public function __construct(\PDO $pdo) {
$this->_link = $pdo;
}
public function getLink() {
return $this->_link;
}
//other useful stuff
}
class user {
private $_db;
public function __construct(\database $db) {
$this->_db = $db;
}
public function getDB() {
return $this->_db;
}
/**
* Returns the ID of a user.
* #param string $user
* #return mixed
*/
public function getUserID($user) {
$query = $this->getDB()->getLink()->prepare("SELECT `user_id` FROM `users` WHERE `username` = :user");
$query->bindParam(":user", $user);
$query->execute();
$result = $query->fetch(PDO::FETCH_ASSOC);
return $result['user_id'];
}
/**
* Checks if given user is active.
* #param string $user
* #return bool Returns true/false if user is active.
*/
public function isUserActive($user) {
}
}
Usage
$user = new \user(new \database(new \PDO("mysql:host=localhost; dbname=swinkidc_student", "swinkidc_student", "")));
Now you can configure your db connection outside of the class.

prepared statements and wrong lastInsertId

I have a very weird behaviour with PDO. I won't go into much details as it would take up way too much time but basically what I observed is that when I re-use a \PDOStatement that performs a simple INSERT I sistematically get a wrong value when invoking PDO::lastInsertId().
The first time I execute the statement it works fine and I get back the right id. Subsequent executions will instead always return '0'. This is even more weird because it happens only between tests (PHPUnit ones). So say I execute the insert using the prepared statement in test1 (working), in test2 it will fail miserably.
When executing multiple times the prepared statement in a non unit-testing environment (in a simple php file fro instance) it all works fine and the last inserted ids are always accurate. Very weird indeed.
Here's the test (note that PersistencyManagerInstance is just a plain intsance of PersistencyManager):
<?php
class PersistencyManagerTest extends PHPUnit_Framework_TestCase {
const DELETE_ALL = "TRUNCATE user";
const ADD_USER = "INSERT INTO user values(null, :username, :password)";
const CHECK_USER_EXISTENCE = "SELECT * FROM user WHERE username = :username AND password = :password";
const DELETE_USER_BY_ID = "DELETE FROM user WHERE id = ?";
protected $manager = null;
public function __construct() {
$this->manager = new PersistencyManagerInstance(PDOFactory::build());
}
public function setUp() {
$this->manager->exec(self::DELETE_ALL);
}
public function tearDown() {
$this->manager->exec(self::DELETE_ALL);
}
public function testInsert() {
$user = new User("laurent", "password");
$id = $this->manager->insert(self::ADD_USER, $user->export());
$this->assertEquals("1", $id);
}
public function testInsertAgain() {
$user1 = new User("laurent1", "password1");
$id = $this->manager->insert(self::ADD_USER, $user1->export());
$this->assertEquals("1", $id);
}
public function testQuery() {
$user = new User("laurent", "password");
$this->manager->insert(self::ADD_USER, $user->export());
$results = $this->manager->query(self::CHECK_USER_EXISTENCE, $user->export());
$this->assertEquals(1, count($results));
}
public function testExec() {
$user = new User("laurent", "password---");
$id = $this->manager->insert(self::ADD_USER, $user->export());
$affected = $this->manager->exec(self::DELETE_USER_BY_ID, array($id));
$this->assertEquals(1, $affected);
}
}
testInsert works while testInsertAgain does not.
and here's the class:
<?php
namespace memory\manager;
use \PDO;
abstract class PersistencyManager {
/**
* #var array An array of \PDOStatement objects
*/
protected static $ps = array();
/**
* #var \PDO
*/
protected $connection = null;
protected function prepareStmt($sql) {
// return $this->connection->prepare($sql);
$key = md5($sql);
if (!isset(self::$ps[$key])) {
self::$ps[$key] = $this->connection->prepare($sql);
}
return self::$ps[$key];
}
public function __construct(PDO $connection) {
$this->connection = $connection;
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function __destruct() {
$this->connection = null;
}
/**
* Good for SELECT operations. By default it fetches using arrays.
* #param string $sql
* #param array $values
* #param integer $fetchStyle
* #return array A list of matching elements (The elements' type depends on $fetchStyle)
*/
public function query($sql, array $values = array(), $fetchStyle = PDO::FETCH_ASSOC) {
$prepared = $this->prepareStmt($sql);
$prepared->execute($values);
$prepared->setFetchMode($fetchStyle);
$all = $prepared->fetchAll();
$prepared->closeCursor();
return $all;
}
/**
* Good for INSERT operations.
* #param string $sql
* #param array $values
* #return string Last inserted element's id in string format
*/
public function insert($sql, array $values = array()) {
$prepared = $this->prepareStmt($sql);
$prepared->execute($values);
$prepared->closeCursor();
return $this->connection->lastInsertId();
}
/**
* Good for all the remaining routines.
* #param string $sql
* #param array $values
* #return integer The number of effected rows
*/
public function exec($sql, array $values = array()) {
$prepared = $this->prepareStmt($sql);
$prepared->execute($values);
$count = $prepared->rowCount();
$prepared->closeCursor();
return $count;
}
}
Any idea?
Cheers
guys I was starting a new connection at every test. That was the reason.

Categories