Using
repository pattern with Eloquent
for code that lasts
Who am I?
<?php
$me = [
'name' => 'Damiano',
'surname' => 'Petrungaro',
'job' => 'Software Developer',
'company' => [
'name' => 'Car Match',
'url' => 'https://carmatch.mx/'
],
'github' => 'https://github.com/damianopetrungaro',
'twitter' => '@damiano_dev',
'buzzwords' => [
'food', 'php', 'code', 'perfectionist', 'DDD', 'never stop learning'
]
];
Me(ow) at work
CATS ALERT!
Eloquent
The Eloquent ORM included with Laravel provides a beautiful, simple ActiveRecord implementation for working with your database.
- Laravel documentation
Active Record
An object that wraps a row
in a database table or view,
encapsulates the database access,
and adds domain logic on that data.
- Martin Fowler, PoEAA
Data Mapper, Table Data Gateway, Row Data Gateway
BUSINESS LOGIC
PERSISTENCE
USER
PRODUCT
CART
<?php
namespace App\Http\Controllers;
//...
class UserController extends Controller
{
public function show(Request $request)
{
$name = $request->get('name', 'Damiano');
return UserEloquent::where('name', $name)->first() ?: "No $name was found";
}
}
How eloquent models are usually used
Read
<?php
namespace App\Http\Controllers;
class UserController extends Controller
{
public function create(Request $request)
{
// Some validation logic here
$user = new UserEloquent();
$user->name = $request->name;
// Some domain logic here
$user->save();
return $user
}
}
How eloquent models are usually used
Write
Every time a breaking change is introduced in the model
SRP
A class should have only a single responsibility
Changes to only one part of the software's specification should be able to affect the specification of the class
Low flexibility & high coupling
One to one mapping between table structure and entity
One property of an Entity will be a column in a table or a field in a document
Our domain model depends on ORM behavior by design
Our model can't exist without the ORM and exposes behavior not needed by the domain
Repository pattern
to the rescue!
Repository pattern
Mediates between the domain
and data mapping layers
using a collection-like interface
for accessing domain objects.
- Edward Hieatt and Rob Mee, PoEAA
BUSINESS LOGIC
PERSISTENCE
USER
REPOSITORY
PRODUCT
REPOSITORY
CART
REPOSITORY
USER
PRODUCT
CART
<?php
namespace App\Repository;
// ...
class UserRepository
{
public function __construct(UserEloquent $userModel)
{
$this->userModel = $userModel;
}
public function findByName(string $name):? UserEloquent
{
return $this->userModel->where('name', $name)->first();
}
}
Free to use all the benefits of the
active record <3
<?php
namespace App\Http\Controllers;
//...
class UserController extends Controller
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function show(Request $request)
{
$name = $request->get('name', 'Damiano');
return $this->userRepository->findByName($name) ?: "No $name were found";
}
}
<?php
namespace App\Http\Controllers;
class UserController extends Controller
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function show(Request $request)
{
$name = $request->get('name', 'Damiano');
if ($user = $this->userRepository->findByName($name)) {
// We do not want this
// $user->delete();
}
return $user ?: "No $name were found";
}
}
Inject the dependecy where needed
...but
<?php
namespace App\Model;
interface User
{
public function id(): int;
public function name(): string;
}
<?php
namespace App\Model;
// ...
class UserEloquent extends Model implements User
{
// ...
public function id(): int
{
return $this->id;
}
public function name(): string
{
return $this->name;
}
}
Use it with an interface
and encapsulate the active record feature only in the repositories
Imagine the repository as
an object that holds eloquent local query scope
<?php
namespace App\Model;
// ...
class UserEloquent extends Model implements User
{
// ...
public function scopeFindByName(string $name)
{
// ...
}
public function scopeBannedLastWeek()
{
// ...
}
}
<?php
namespace App\Repository;
// ...
class UserRepository
{
// ...
public function findByName(string $name):? User
{
// ...
}
public function bannedLastWeek(): UserCollection
{
// ...
}
}
How to 'write' using repositories?
<?php
namespace App\Repository;
// ...
class UserRepository
{
// ...
public function add(User $user): void
{
// ...
}
public function update(User $user): void
{
// ...
}
public function delete(int $id): void
{
// ...
}
// ...
}
Final tips
yes, it can be done better
<?php
namespace App\Repository;
use App\Model\User;
interface UserRepository
{
public function findByName(string $name):? User;
public function add(User $user): void;
public function update(User $user): void;
public function delete(int $id): void;
}
<?php
namespace App\Repository;
// ...
final class UserEloquentRepository implements UserRepository
{
// ...
public function findByName(string $name):? User
{
// ...
}
public function add(User $user): void
{
// ...
}
public function update(User $user): void
{
// ...
}
public function delete(int $id): void
{
// ...
}
}
Use interface for repository too
will save you a lot of time when you need to migrate
<?php
namespace App\Model;
interface User
{
public static fromName(string $name): self;
// ...
}
<?php
namespace App\Model;
// ...
class UserEloquent implements User
{
private function __construct()
{
// Ah! You can't use me!
}
public static function fromName(string $name): self
{
if(strlen($name) <= 5) {
throw new InvalidNameException($name);
}
$user = new self;
$user->name = $name;
return $user;
}
}
Build your entities always in a valid state
... with named constructor
<?php
namespace App\ValueObject;
final class Name
{
// ...
public function __construct(string $name)
{
// All validation rules here
$this->name = $name;
}
}
<?php
namespace App\Model;
// ...
class UserEloquent implements User
{
// ...
private function __construct()
{
// Ah! You can't use me!
}
public static function fromName(Name $name): self
{
$user = new self;
$user->name = $name;
return $user;
}
// ...
}
Build your entities always in a valid state
... using value objects
Repository easier to maintain
<?php
namespace App\Repository;
// ...
interface UserRepository
{
public function findByEmail(Email $email):? User;
public function findByName(Name $name):? User;
public function getById(UserId $id): User;
public function add(User $user): void;
public function update(User $user): void;
public function delete(UserId $id): void;
}
-
Isolate read and write operation
-
Domain model won't be anemic but rich
-
Maintain and migrate can't be easier
- Isolate bugs focusing on domain model
-
Still using your preferred ORM under the hood
- Easier to test
Recap
Questions?
Thank you!
twitter: @damiano_dev
slides: http://bit.ly/dp-eloquent
joind: https://joind.in/talk/c94e6
Using Active Record
By Damiano Petrungaro
Using Active Record
- 1,616