El patrón de repositorio ha ganado bastante popularidad desde que se introdujo por primera vez como parte del diseño basado en dominios en 2004. Esencialmente, proporciona una abstracción de datos, de modo que su aplicación puede funcionar con una abstracción simple que tiene una interfaz que se aproxima el de una colección. La adición, eliminación, actualización y selección de elementos de esta colección se realiza a través de una serie de métodos sencillos, sin la necesidad de lidiar con problemas de bases de datos como conexiones, comandos, cursores o lectores. El uso de este patrón puede ayudar a lograr un acoplamiento flexible y puede mantener ignorante la persistencia de los objetos de dominio. Aunque el patrón es muy popular (o quizás debido a esto), también se malinterpreta y se utiliza con frecuencia. Hay muchas formas diferentes de implementar el patrón Repository. Consideremos algunos de ellos y sus méritos e inconvenientes.
Crear el archivo de composer.json
composer.json
{
"name": "repository-pattern/php-7",
"autoload": {
"psr-4": {
"Sales\\": "sales/",
"App\\": "app/"
}
}
}Generar/Actualizar el paquete de Vendor para las autocargas.
composer dump-autoloadCrear los directorios especificados en el psr-4. Dentro del directorio de Ventas crear la carpeta Database para crear una clase para el Provider de la base de datos.
sales/Database.php
<?php
namespace Sales\Database;
use Pdo;
class DbProvider
{
private static $_db;
public static function get()
{
if (!self::$_db) {
$pdo = new Pdo(
__CONFIG__['db']['host'],
__CONFIG__['db']['user'],
__CONFIG__['db']['password']
);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
self::$_db = $pdo;
}
return self::$_db;
}
}Los modelos son las entidades que hacen referencia a las tablas. Crear las clases modelos.
app/Models/Order.php
<?php
namespace App\Models;
class Order
{
public $id;
// User client
public $user_id;
public $client;
public $total = 0;
// User creater
public $creater_id;
public $creater;
public $created_at;
public $updated_at;
// Order Detail
public $detail = [];
}app/Models/OrderDetail.php
<?php
namespace App\Models;
class OrderDetail
{
public $id;
public $order_id;
// Product
public $product_id;
public $product;
public $quantity;
public $price;
public $total = 0;
public $created_at;
public $updated_at;
}app/Models/Product.php
<?php
namespace App\Models;
class Product
{
public $id;
public $name;
public $price;
public $created_at;
public $updated_at;
}app/Models/User.php
<?php
namespace App\Models;
class User
{
public $id;
public $first_name;
public $last_name;
public $user_name;
public $password;
public $created_at;
public $updated_at;
}Los service son los encargados de consumir los repositorios y mapear los modelos. Crear las clases services.
app/Services/OrderService.php
<?php
namespace App\Services;
use App\Models\Order;
use App\Repositories\{OrderRepository, UserRepository, OrderDetailRepository, ProductRepository};
use PDOException;
class OrderService
{
private $_userRepository;
private $_orderRepository;
private $_orderDetailRepository;
private $_productRepository;
public function __construct()
{
$this->_userRepository = new UserRepository();
$this->_orderRepository = new OrderRepository();
$this->_orderDetailRepository = new OrderDetailRepository();
$this->_productRepository = new ProductRepository();
}
public function get(int $id): ?Order
{
$result = null;
try {
// Begin transacation
$this->_db->beginTransaction();
$data = $this->_orderRepository->find($id);
if ($data) {
$result = $data;
// Client
$result->client = $this->_userRepository->find($result->user_id);
// Creater
$result->creater = $this->_userRepository->find($result->creater_id);
// Detail
$result->detail = $this->getDetail($result->id);
}
} catch (PDOException $ex) {
$this->_db->rollBack();
print($ex->getMessage());
}
return $result;
}
private function getDetail(int $order_id): array
{
$result = [];
try {
// Begin transacation
$this->_db->beginTransaction();
$result = $this->_orderDetailRepository->findAllByOrderId($order_id);
foreach ($result as $item) {
$item->product = $this->_productRepository->find($item->product_id);
}
} catch (PDOException $ex) {
$this->_db->rollBack();
print($ex->getMessage());
}
return $result;
}
public function create(Order $model): void
{
try {
// Begin transacation
$this->_db->beginTransaction();
// Prepare order creation
$this->prepareOrderCreation($model);
// Order creation
$this->_orderRepository->add($model);
// Order Detail creation
$this->_orderRepository->addByOrderId($model->id, $model->detail);
// Commit transaction
$this->_db->commit();
} catch (PDOException $ex) {
$this->_db->rollBack();
print($ex->getMessage());
}
}
private function prepareOrderCreation(Order &$model): void
{
$now = date('Y-m-d H:i:s');
$model->created_at = $now;
$model->updated_at = $now;
foreach ($model->detail as $item) {
$item->total = $item->price * $item->quantity;
$item->created_at = $now;
$item->updated_at = $now;
$model->total += $item->total;
}
}
}app/Services/ProductService.php
<?php
namespace App\Services;
use App\Models\Product;
use App\Repositories\ProductRepository;
use PDOException;
class ProductService
{
private $_productRepository;
public function __construct()
{
$this->_productRepository = new ProductRepository();
}
public function getAll(): array
{
$result = [];
try {
// Begin transacation
$this->_db->beginTransaction();
$result = $this->_productRepository->findAll();
} catch (PDOException $ex) {
print $ex->getMessage();
}
return $result;
}
public function get(int $id): ?Product
{
$result = null;
try {
// Begin transacation
$this->_db->beginTransaction();
$result = $this->_productRepository->find($id);
} catch (PDOException $ex) {
$this->_db->rollBack();
print $ex->getMessage();
}
return $result;
}
public function create(Product $model): void
{
try {
// Begin transacation
$this->_db->beginTransaction();
$this->_productRepository->add($model);
} catch (PDOException $ex) {
$this->_db->rollBack();
print $ex->getMessage();
}
}
public function update(Product $model): void
{
try {
// Begin transacation
$this->_db->beginTransaction();
$this->_productRepository->update($model);
} catch (PDOException $ex) {
$this->_db->rollBack();
print $ex->getMessage();
}
}
public function delete(int $id): void
{
try {
// Begin transacation
$this->_db->beginTransaction();
$this->_productRepository->remove($id);
} catch (PDOException $ex) {
$this->_db->rollBack();
print $ex->getMessage();
}
}
}Crear repositorios los cuales realizaran las transacciones con la base de datos.
Repositories/OrderDetailRepository.php
<?php
namespace App\Repositories;
use Sales\Database\DbProvider;
use PDO;
class OrderDetailRepository
{
private $_db;
public function __construct()
{
$this->_db = DbProvider::get();
}
public function findAllByOrderId(int $order_id): Array
{
$result = [];
// 01. Prepare query
$stm = $this->_db->prepare('select * from order_detail where order_id = :order_id');
// 02. Execute query
$stm->execute(['order_id' => $order_id]);
// 03. Fetch All
$data = $stm->fetchAll(\PDO::FETCH_CLASS, '\\App\\Models\\OrderDetail');
// 04. Verify if result is null
if ($data) {
$result = $data;
}
return $result;
}
}Repositories/OrderRepository.php
<?php
namespace App\Repositories;
use Sales\Database\DbProvider;
use App\Models\Order;
use PDO;
class OrderRepository
{
private $_db;
public function __construct()
{
$this->_db = DbProvider::get();
}
public function find(int $id): ?Order
{
$result = null;
// 01. Prepare query
$stm = $this->_db->prepare('select * from orders where id = :id');
// 02. Execute query
$stm->execute(['id' => $id]);
// 03. Fetch All
$data = $stm->fetchObject('\\App\\Models\\Order');
// 04. Verify if result is null
if ($data) {
$result = $data;
}
return $result;
}
public function findAll(): array
{
$result = [];
// 01. Prepare query
$stm = $this->_db->prepare('select * from orders');
// 02. Execute query
$stm->execute();
// 03. Fetch All
$data = $stm->fetchAll(PDO::FETCH_CLASS, '\\App\\Models\\Order');
// 04. Verify if result is null
if ($data) {
$result = $data;
}
return $result;
}
public function add(Order $model): void
{
// 01. Prepare query
$stm = $this->_db->prepare('
insert into orders(user_id, total, creater_id, created_at, updated_at)
values(:user_id, :total, :creater_id, :created, :updated)
');
// 02. Execute query
$stm->execute([
'user_id' => $model->user_id,
'total' => $model->total,
'creater_id' => $model->creater_id,
'created' => $model->created_at,
'updated' => $model->updated_at,
]);
$model->id = $this->_db->lastInsertId();
}
public function addByOrderId(int $orderId, array $model): void
{
foreach ($model as $item) {
$stm = $this->_db->prepare('
insert into order_detail(order_id, product_id, quantity, price, total, created_at, updated_at)
values(:order_id, :product_id, :quantity, :price, :total, :created_at, :updated_at)
');
$stm->execute([
'order_id' => $orderId,
'product_id' => $item->product_id,
'quantity' => $item->quantity,
'price' => $item->price,
'total' => $item->total,
'created_at' => $item->created_at,
'updated_at' => $item->updated_at,
]);
}
}
public function remove(int $id): void
{
// 01. Prepare query
$stm = $this->_db->prepare(
'delete from orders where id = :id'
);
// 02. Execute query
$stm->execute(['id' => $id]);
}
}Repositories/ProductRepository.php
<?php
namespace App\Repositories;
use Sales\Database\DbProvider;
use App\Models\Product;
use PDO;
class ProductRepository
{
private $_db;
public function __construct()
{
$this->_db = DbProvider::get();
}
public function find(int $id): ?Product
{
$result = null;
// 01. Prepare query
$stm = $this->_db->prepare('select * from products where id = :id');
// 02. Execute query
$stm->execute(['id' => $id]);
// 03. Fetch Single
$data = $stm->fetchObject('\\App\\Models\\Product');
// 04. Verify if result is null
if ($data) {
$result = $data;
}
return $result;
}
public function findAll(): array
{
$result = [];
// 01. Prepare query
$stm = $this->_db->prepare('select * from products');
// 02. Execute query
$stm->execute();
// 03. Fetch All
$data = $stm->fetchAll(PDO::FETCH_CLASS, '\\App\\Models\\Product');
// 04. Verify if result is null
if ($data) {
$result = $data;
}
return $result;
}
public function add(Product $model): void
{
// 01. Prepare query
$stm = $this->_db->prepare(
'insert into products(name, price, created_at, updated_at) values (:name, :price, :created, :updated)'
);
$now = date('Y-m-d H:i:s');
// 02. Execute query
$stm->execute([
'name' => $model->name,
'price' => $model->price,
'created' => $now,
'updated' => $now,
]);
}
public function update(Product $model): void
{
// 01. Prepare query
$stm = $this->_db->prepare('
update products
set
name = :name,
price = :price,
updated_at = :updated
where id = :id
');
// 02. Execute query
$stm->execute([
'name' => $model->name,
'price' => $model->price,
'updated' => date('Y-m-d H:i:s'),
'id' => $model->id,
]);
}
public function remove(int $id): void
{
// 01. Prepare query
$stm = $this->_db->prepare(
'delete from products where id = :id'
);
// 02. Execute query
$stm->execute(['id' => $id]);
}
}Repositories/UserRepository.php
<?php
namespace App\Repositories;
use Sales\Database\DbProvider;
use App\Models\User;
use PDO;
class UserRepository
{
private $_db;
public function __construct()
{
$this->_db = DbProvider::get();
}
public function find(int $id): ?User
{
$result = null;
// 01. Prepare query
$stm = $this->_db->prepare('select * from users where id = :id');
// 02. Execute query
$stm->execute(['id' => $id]);
// 03. Fetch Single
$data = $stm->fetchObject('\\App\\Models\\User');
// 04. Verify if result is null
if ($data) {
$result = $data;
}
return $result;
}
public function findAll(): array
{
$result = [];
// 01. Prepare query
$stm = $this->_db->prepare('select * from users');
// 02. Execute query
$stm->execute();
// 03. Fetch All
$data = $stm->fetchAll(PDO::FETCH_CLASS, '\\App\\Models\\User');
// 04. Verify if result is null
if ($data) {
$result = $data;
}
return $result;
}
public function add(User $model): void
{
// 01. Prepare query
$stm = $this->_db->prepare(
'insert into users(first_name, last_name, user_name, password, created_at, updated_at) values (:first_name, :last_name, :user_name, :password, :created, :updated)'
);
$now = date('Y-m-d H:i:s');
// 02. Execute query
$stm->execute([
'first_name' => $model->first_name,
'last_name' => $model->last_name,
'user_name' => $model->user_name,
'password' => $model->password,
'created' => $now,
'updated' => $now,
]);
}
public function update(User $model): void
{
// 01. Prepare query
$stm = $this->_db->prepare('
update users
set
first_name = :first_name,
last_name = :last_name,
user_name = :user_name,
password = : password,
updated_at = :updated
where id = :id
');
// 02. Execute query
$stm->execute([
'name' => $model->name,
'price' => $model->price,
'updated' => date('Y-m-d H:i:s'),
'id' => $model->id,
]);
}
public function remove(int $id): void
{
// 01. Prepare query
$stm = $this->_db->prepare(
'delete from users where id = :id'
);
// 02. Execute query
$stm->execute(['id' => $id]);
}
}Agregar el paquete de dependencia de monolog para gestionar logs.
composer require monolog/monologCrear una clase de contenedor.
sales/Container.php
<?php
namespace Sales;
class Container
{
private static $dependencies = [];
public static function set(string $key, $func)
{
self::$dependencies[$key] = $func;
}
public static function get(string $key)
{
return self::$dependencies[$key]();
}
}Agregar al archivo de Config.php los parametros para inicializar monolog.
config.php
<?php
define('__CONFIG__', [
'db' => [
'host' => 'mysql:host=localhost;dbname=sales_db;charset=utf8',
'user' => 'root',
'password' => ''
],
'log' => [
'path' => 'log/',
'channel' => 'sales'
]
]);Creamos el archivo inicial donde probaremos esta solución.
index.php
<?php
require_once 'vendor/autoload.php';
require_once 'config.php';
use Sales\Container;
Container::set('logger', function () {
$logger = new \Monolog\Logger(__CONFIG__['log']['channel']);
//Handler para trabajar con archivos de texto
$file_handler = new \Monolog\Handler\StreamHandler(__CONFIG__['log']['path'] . date('Ymd') . '.log');
//Agregar Handler
$logger->pushHandler($file_handler);
return $logger;
});
// Test Log
$logger = Container::get('logger');
$logger->info('Project started');Tener en cuenta los niveles de los Logs, leer más al respecto en https://github.com/Seldaek/monolog/blob/master/doc/01-usage.md
Monolog supports the logging levels described by RFC 5424.
DEBUG (100): Detailed debug information.
INFO (200): Interesting events. Examples: User logs in, SQL logs.
NOTICE (250): Normal but significant events.
WARNING (300): Exceptional occurrences that are not errors. Examples: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong.
ERROR (400): Runtime errors that do not require immediate action but should typically be logged and monitored.
CRITICAL (500): Critical conditions. Example: Application component unavailable, unexpected exception.
ALERT (550): Action must be taken immediately. Example: Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up.
EMERGENCY (600): Emergency: system is unusable.
Agregar el control de logs en los servicios.
<?php
namespace App\Services;
use Sales\Container;
class ExampleService
{
//otros atributos
private $_logger;
//constructor
public function __construct()
{
//instancia de otros atributos
$this->_logger = Container::get('logger');
}
public function someFunction()
{
try{
$this->logger->info('Empezó el proceso del bloque'); //log info
//bloque de codigo
$this->logger->info('Se completo el proceso del bloque'); //log info
} catch(PDOException $ex){
$this->logger->error($ex->getMessage());
}
}
}