I am attempting to clean up my code in my Laravel models at the moment and at the moment I have a lot of code duplication with local scopes in each model. Each model has a lot of similar columns that relate and I am figuring out how I can put these scopes in a single place but still be able to pass the $table variable so it can be used dynamically.
What I currently have in a few models for example
public function scopeName($query, $name)
{
return $query->where($this->table.'name', $name);
}
public function scopeStatus($query, $status)
{
return $query->where($this->table.'.status', $status);
}
I want to take these common scopes and have them in a single class where I can pass the table to it and then I can eliminate the code duplication. Haven't been able to find anything similar with extending the parent Model. I am also aware of global scopes but that still creates a similar issue where I am typing static::addGlobalScope(new StatusScope) in each model.
Any help is greatly appreciated.
Would using a trait be considered a bad option here?
Trait is better here than inheritance, read more: https://en.wikipedia.org/wiki/Composition_over_inheritance
You can create a new trait for it:
trait CommonScopesTrait
{
public function scopeName($query, $name)
{
return $query->where($this->table.'name', $name);
}
public function scopeStatus($query, $status)
{
return $query->where($this->table.'.status', $status);
}
}
And use it in your model(s):
class YourModel extends Model
{
use CommonScopesTrait;
}
In addition to the answer of lptn, there is another option, which is also quite nice.
Create a basic model which you extend to Eloquent:
class BasicScope extends Eloquent {
public function scopeName($query, $name)
{
return $query->where($this->table.'name', $name);
}
public function scopeStatus($query, $status)
{
return $query->where($this->table.'.status', $status);
}
}
And then extend your own models on the BasicScope model, so you will have those methods available:
class User extends BasicScope {
}
Related
When I read the Laravel 5.3's source code, I find the following codes
public function withGlobalScope($identifier, $scope)
{
$this->scopes[$identifier] = $scope;
if (method_exists($scope, 'extend')) {
$scope->extend($this);
}
return $this;
}
I am confused about why it is the following codes in the method, is it used anywhere?
if (method_exists($scope, 'extend')) {
$scope->extend($this);
}
Thanks!
It's meant for extending the Builder within the context of a Scope class.
Inside a global Scope (class which extends the Scope interface), you can create an extend function next to the apply function.
This extend function is called with the Eloquent builder as parameter. Imagine some PopularUsersScope, which only gets very popular users:
public function apply(Builder $builder, Model $model): void
{
$builder->join(...)->where(...) // determine popularity
}
public function extend(Builder $builder)
{
$builder->macro('demote', function (Builder $builder) {
return $builder->update(...) // query to make user not so popular
});
}
Now lets have a model Clan, which is a group of users, some of which are popular. We have a constrained relation on Clan:
public function popular_users(): belongsToMany
{
return $this->hasMany(User::class)
->withGlobalScope('popularUsers', new PopularUsersScope)
}
Because we defined a macro within the scope extend function, we can do $user->demote() for any popular user, but not for normal users.
This might be a bit of a silly example, but it can be useful in fairly abstract use cases.
See SoftDeletingScope in the Laravel Framework code for a practical example: SoftDeletable items need some extra methods on the Builder for restoring and eager loading deleted items.
Extend in SoftDeletingScope dynamically adds multiple extensions to the Builder, from an Array $this->extensions, and registers an replacement for the default delete function on the model:
public function extend(Builder $builder)
{
foreach ($this->extensions as $extension) {
$this->{"add{$extension}"}($builder);
}
$builder->onDelete(function (Builder $builder) {
$column = $this->getDeletedAtColumn($builder);
return $builder->update([
$column => $builder->getModel()->freshTimestampString(),
]);
});
}
I'm currently rebuilding my vanilla-PHP-App with Laravel and I have the following problem.
I have multiple database-tables, that represent word categories (noun, verb, adverb, ...). For each table I created a separate Model, a route::resource and a separate resource-Controller. For example:
NomenController.php
public function show($id)
{
$vocab = Nomen::find($id);
return view('glossarium.vocab_update', compact('vocab'));
}
and
VerbController.php
public function show($id)
{
$vocab = Verb::find($id);
return view('glossarium.vocab_update', compact('vocab'));
}
...which are essentially the same except the Model class.
I don't want to create a separate Controller for each model, that does exactly the same. What would be the most simple and elegant way to solve this?
Should I just create a VocabController.php and add a parameter for the Model-name like:
Route::resource('/vocab/{category}', 'VocabController');
and then add a constructor method in this controller like
public function __construct ($category) {
if ($category == 'nomen') {
$this->vocab = App\Nomen;
}
else if ($category == 'verb') {
$this->vocab = App\Verb;
}
}
I wonder if there is a simpler method to do that. Can I somehow do this with Route Model Binding?
Thanks in advance
Simply create a trait like this in App\Traits, (you can name it anything... Don't go with mine though... I feel its pretty lame... :P)
namespace App\Traits;
trait CommonControllerFunctions {
public function show($id) {
$modelObject = $this->model;
$model = $modelObject::find($id);
return view('glossarium.vocab_update', compact('model'));
}
}
and in your NomenController and VerbController, do this:
use App\Traits\CommonControllerFunctions;
class NomenController {
use CommonControllerFunctions;
protected $model = Nomen::class;
}
and
use App\Traits\CommonControllerFunctions;
class VerbController {
use CommonControllerFunctions;
protected $model = Verb::class;
}
Note: Please note that this example is just a work-around for your particular situation only... Everyone practices code differently, so this method might not be approved by all...
I think the simpliest way it to create only one controller, eg VocabController with methods nomen, verb and whatever you want.
Routes:
Route::get('/vocab/nomen/{nomen}', 'VocabController#item');
Route::get('/vocab/verb/{verb}', 'VocabController#item');
And the model binding:
Route::model('nomen', 'App\Nomen');
Route::model('verb', 'App\Varb');
Then your method shoud look like that:
public function item($item)
{
return view('glossarium.vocab_update', $item);
}
Keep in mind, that $item is already fetched model from the database.
Concept Problem:
I have a very simple problem when using the touches attribute, to automatically update timestamp on a depending model; it correctly does so but also applies the global scopes.
Is there any way to turn this functionality off? Or to ask specifically for automatic touches to ignore global scopes?
Concrete Example:
When an ingredient model is updated all related recipes should be touched. This works fine, except we have a globalScope for separating the recipes based on locales, that also gets used when applying the touches.
Ingredient Model:
class Ingredient extends Model
{
protected $touches = ['recipes'];
public function recipes() {
return $this->belongsToMany(Recipe::class);
}
}
Recipe Model:
class Recipe extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new LocaleScope);
}
public function ingredients()
{
return $this->hasMany(Ingredient::class);
}
}
Locale Scope:
class LocaleScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$locale = app(Locale::class);
return $builder->where('locale', '=', $locale->getLocale());
}
}
If you want to explicitly avoid a global scope for a given query, you may use the withoutGlobalScope() method. The method accepts the class name of the global scope as its only argument.
$ingredient->withoutGlobalScope(LocaleScope::class)->touch();
$ingredient->withoutGlobalScopes()->touch();
Since you're not calling touch() directly, in your case it will require a bit more to make it work.
You specify relationships that should be touched in model $touches attribute. Relationships return query builder objects. See where I'm going?
protected $touches = ['recipes'];
public function recipes() {
return $this->belongsToMany(Recipe::class)->withoutGlobalScopes();
}
If that messes with the rest of your application, just create a new relationship specifically for touching (heh :)
protected $touches = ['recipesToTouch'];
public function recipes() {
return $this->belongsToMany(Recipe::class);
}
public function recipesToTouch() {
return $this->recipes()->withoutGlobalScopes();
}
You can define a relationship in the model and pass parameters to it like following:
public function recipes($isWithScope=true)
{
if($isWithScope)
return $this->belongsToMany(Recipe::class);
else
return $this->recipes()->withoutGlobalScopes();
}
then use it like this recipes->get(); and recipes(false)->get();
In Laravel 4, query scopes are available on all queries (including ones generated by relations queries). This means that for the following (example) models:
Customer.php:
<?php
class Customer extends Eloquent {
public function order() { return $this->hasMany('Order'); }
}
Order.php:
<?php
class Order extends Eloquent {
public function scopeDelivered($query) { return $query->where('delivered', '=', true); }
public function customer() { return $this->belongsTo('Customer'); }
}
Both of the following work:
var_dump(Order::delivered()->get()); // All delivered orders
var_dump(Customer::find(1)->orders()->delivered()->get()); // only orders by customer #1 that are delivered
This is useful from within a controller because the query logic for finding delivered orders doesn't have to be repeated.
Recently, though, I've been convinced that the Repository pattern is optimal for not only separation of concerns but also for the possibility of a ORM/DB switch or the necessity of adding middleware like a cache. Repositories feel very natural, because now instead of having scopes bloat my models, the associated queries are instead part of the Repository (which makes more sense because naturally this would be a method of the collection not the item).
For example,
<?php
class EloquentOrderRepository {
protected $order;
public function __construct(Order $order) { $this->order = $order; }
public function find($id) { /* ... */ }
/* etc... */
public function allDelievered() { return $this->order->where('delivered', '=', true)->get(); }
}
However, now I have the delivered scope repeated, so to avoid violating DRY, I remove it from the model (which seems logical as per the justification above). But now, I can no longer can use scopes on relations (like $customer->orders()->delivered()). The only workaround here I see is somehow instantiating the Repository with the pre-made query (similar to what is passed to the scopes in the models) in the Relation base class. But this involves changing (and overriding) a lot of code and default behavior and seems to make things more coupled than they should be.
Given this dilemma, is this is misuse of a repository? If not, is my solution the only way to regain the functionality that I would like? Or is having the scopes in the models not tight enough coupling to justify this extra code? If the scopes aren't tight coupling, then is there a way to use both the Repository pattern and scopes while still being DRY?
Note: I am aware of some similar questions on similar topics but none of them address the issue presented here with queries generated by relationships, which do not rely on the Repository.
I've managed to find a solution. It's rather hacky and I'm not sure whether I consider it acceptable (it uses a lot of things in ways that they likely weren't meant to be used). To summarize, the solution allows you to move scopes to the repository. Each repository (on instantiation) is booted once, and during this process all of the scope methods are extracted and added to each query created by the eloquent model (via macros) by way of a Illuminate\Database\Eloquent\ScopeInterface.
The (Hack-y) solution
Repository Pattern Implementation
app/lib/PhpMyCoder/Repository/Repository.php:
<?php namespace PhpMyCoder\Repository;
interface Repository {
public function all();
public function find($id);
}
app/lib/PhpMyCoder/Repository/Order/OrderRepository.php:
<?php namespace PhpMyCoder\Repository\Order;
interface OrderRepository extends PhpMyCoder\Repository\Repository {}
Adding Eloquent Repositories (and a hack)
app/lib/PhpMyCoder/Repository/Order/EloquentOrderRepository.php:
<?php namespace PhpMyCoder\Repository\Order;
use PhpMyCoder\Repository\EloquentBaseRepository;
class EloquentOrderRepository extends EloquentBaseRepository implements OrderRepository {
public function __construct(\Order $model) {
parent::__construct($model);
}
public function finished() {
return $this->model->finished()->get();
}
public function scopeFinished($query) {
return $query->where('finished', '=', true);
}
}
Notice how the repository contains the scope that would normally be stored in the Order model class. In the database (for this example), Order needs to have a boolean column finished. We'll cover the details of EloquentBaseRepository below.
app/lib/PhpMyCoder/Repository/EloquentBaseRepository.php:
<?php namespace PhpMyCoder\Repository;
use Illuminate\Database\Eloquent\Model;
abstract class EloquentBaseRepository implements Repository {
protected $model;
// Stores which repositories have already been booted
protected static $booted = array();
public function __construct(Model $model) {
$this->model = $model;
$this->bootIfNotBooted();
}
protected function bootIfNotBooted() {
// Boot once per repository class, because we only need to
// add the scopes to the model once
if(!isset(static::$booted[get_class($this)])) {
static::$booted[get_class($this)] = true;
$this->boot();
}
}
protected function boot() {
$modelScope = new ModelScope(); // covered below
$selfReflection = new \ReflectionObject($this);
foreach (get_class_methods($this) as $method) {
// Find all scope methods in the repository class
if (preg_match('/^scope(.+)$/', $method, $matches)) {
$scopeName = lcfirst($matches[1]);
// Get a closure for the scope method
$scopeMethod = $selfReflection->getMethod($method)->getClosure($this)->bindTo(null);
$modelScope->addScope($scopeName, $scopeMethod);
}
}
// Attach our special ModelScope to the Model class
call_user_func([get_class($this->model), 'addGlobalScope'], $modelScope);
}
public function __call($method, $arguments) {
// Handle calls to scopes on the repository similarly to
// how they are handled on Eloquent models
if(method_exists($this, 'scope' . ucfirst($method))) {
return call_user_func_array([$this->model, $method], $arguments)->get();
}
}
/* From PhpMyCoder\Repository\Order\OrderRepository (inherited from PhpMyCoder\Repository\Repository) */
public function all() {
return $this->model->all();
}
public function find($id) {
return $this->model->find($id);
}
}
Each time an instance of a repository class is instantiated for the first time, we boot the repository. This involves aggregating all "scope" methods on the repository into a ModelScope object and then applying that to the model. The ModelScope will apply our scopes to each query created by the model (as seen below).
app/lib/PhpMyCoder/Repository/ModelScope.php:
<?php namespace PhpMyCoder\Repository;
use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Database\Eloquent\Builder;
class ModelScope implements ScopeInterface {
protected $scopes = array(); // scopes we need to apply to each query
public function apply(Builder $builder) {
foreach($this->scopes as $name => $scope) {
// Add scope to the builder as a macro (hack-y)
// this mimics the behavior and return value of Builder::callScope()
$builder->macro($name, function() use($builder, $scope) {
$arguments = func_get_args();
array_unshift($arguments, $builder->getQuery());
return call_user_func_array($scope, $arguments) ?: $builder->getQuery();
});
}
}
public function remove(Builder $builder) {
// Removing is not really possible (no Builder::removeMacro),
// so we'll just overwrite the method with one that throws a
// BadMethodCallException
foreach($this->scopes as $name => $scope) {
$builder->macro($name, function() use($name) {
$className = get_class($this);
throw new \BadMethodCallException("Call to undefined method {$className}::{$name}()");
});
}
}
public function addScope($name, \Closure $scope) {
$this->scopes[$name] = $scope;
}
}
The ServiceProvider and Composer File
app/lib/PhpMyCoder/Repository/RepositoryServiceProvider.php:
<?php namespace PhpMyCoder\Repository;
use Illuminate\Support\ServiceProvider;
use PhpMyCoder\Repository\Order\EloquentOrderRepository;
class RepositoryServiceProvider extends ServiceProvider {
public function register() {
// Bind the repository interface to the eloquent repository class
$this->app->bind('PhpMyCoder\Repository\Order\OrderRepository', function() {
return new EloquentOrderRepository(new \Order);
});
}
}
Be sure to add this service provider to the providers array in the app.php config:
'PhpMyCoder\Repository\RepositoryServiceProvider',
And then add the app/lib to composer's autoload
"autoload": {
"psr-0": {
"PhpMyCoder\\": "app/lib"
},
/* etc... */
},
This will require a composer.phar dump-autoload.
The Models
app/models/Customer.php:
<?php
class Customer extends Eloquent {
public function orders() {
return $this->hasMany('Order');
}
}
Notice that for brevity, I've excluded writing a repository for Customer, but in a real application you should.
app/model/Order.php:
<?php
class Order extends Eloquent {
public function customer() {
return $this->belongsTo('Customer');
}
}
Notice how the scope is not longer stored in the Order model. This makes more structural sense, because the collection level (repository) should be responsible for scopes applying to all orders while Order should only be concerned with details specific to one order. For this demo to work, order must have an integer foreign key customer_id to customers.id and a boolean flag finished.
Usage in the Controller
app/controllers/OrderController.php:
<?php
// IoC will handle passing our controller the proper instance
use PhpMyCoder\Repository\Order\OrderRepository;
class OrderController extends BaseController {
protected $orderRepository;
public function __construct(OrderRepository $orderRepository) {
$this->orderRepository = $orderRepository;
}
public function test() {
$allOrders = $this->orderRepository->all();
// Our repository can handle scope calls similarly to how
// Eloquent models handle them
$finishedOrders = $this->orderRepository->finished();
// If we had made one, we would instead use a customer repository
// Notice though how the relation query also has order scopes
$finishedOrdersForCustomer = Customer::find(1)->orders()->finished();
}
}
Our repository not only contains the scopes for the child model, which is more SOLID. They also come with the ability to handle calls to the scope like a real Eloquent model would. And they add all scopes to each query created by the model so that you have access to them when retrieving related models.
Problems with this Approach
A lot of code for little functionality: arguably too much to accomplish the desired result
It's hacky: macros on Illuminate\Database\Eloquent\Builder and Illuminate\Database\Eloquent\ScopeInterface (in conjunction with Illuminate\Database\Eloquent\Model::addGlobalScope) are likely used in ways they weren't intended to be
It requires instantiation of the repository (MAJOR ISSUE): if you're within the CustomerController and you only have instantiated CustomerRepository, $this->customerRepository->find(1)->orders()->finished()->get() won't work as expected (the finished() macro/scope won't be added to each Order query unless you instantiate OrderRepository).
I'll investigate if there is a more elegant solution (which remedies the issues listed above), but this is the best solution I can find thus far.
Related Resources on the Repository Pattern
Creating flexible Controllers in Laravel 4 using Repositories
Eloquent tricks for better Repositories
I recently watched this video and wanted to change my Laravel controllers so that they had their dependencies managed with Laravel's IoC container. The video talks about creating an interface for a Model and then implementing that interface for the specific data source used.
My question is: when implementing the interface with a class that extends Eloquent and binding that class to the controller so that it is accessible from $this->model, should I also create interfaces and implementations for the Eloquent models which may be returned when calling methods such as $this->model->find($id)? Should there be different classes for the Model and the ModelRepository?
Put it another way: how do I do new Model when my model is in $this->model.
Generally, yes, people doing that pattern (the repository pattern) have an interface which have some methods defined that your app will use:
interface SomethingInterface {
public function find($id);
public function all();
public function paged($offset, $limit);
}
Then you create an implementation of this. If you're using Eloquent, then you can make an Eloquent implementation
use Illuminate\Database\Model;
class EloquentSomething {
protected $something;
public function __construct(Model $something)
{
$this->something = $something;
}
public function find($id)
{
return $this->something->find($id);
}
public function all() { ... }
public function paged($offset, $limit) { ... }
}
Then you make a service provider to put it all together, and add it into app/config/app.php.
use Something; // Eloquent Model
use Namespace\Path\To\EloquentSomething;
use Illuminate\Support\ServiceProvider;
class RepoServiceProvider extends ServiceProvider {
public function register()
{
$app = $this->app;
$app->bind('Namespace/Path/To/SomethingInterface', function()
{
return new EloquentSomething( new Something );
});
}
}
Finally, your controller can use that interface as a type hint:
use Namespace/Path/To/SomethingInterface;
class SomethingController extends BaseController {
protected $something;
public function __construct(SomethingInterface $something)
{
$this->something = $something;
}
public function home() { return $this->something->paged(0, 10); }
}
That should be it. Apologies on any errors, this isn't tested, but is something I do a lot.
Downsides:
More code :D
Upsides:
Able to switch out implementations (instead of EloquentSomething, can use ArraySomething, MongoSomething, whatever), without changing your controller code or any code that uses an implementation of your interface.
Testable - you can mock your Eloquent class and test the repository, or mock your constructor dependency and test your controller
Re-usable - you can App::make() to get the concrete EloquentSomething anywhere in your app and re-use the Something repository anywhere in your code
Repository is a good place to add additional logic, like a layer of cacheing, or even validation rules. Stock mucking about in your controllers.
Finally:, since I likely typed all that out and STILL DIDN'T ANSWER YOUR QUESTION (wtf?!), you can get a new instance of the model using $this->model. Here's an example for creating a new Something:
// Interface:
public function create(array $data);
// EloquentSomething:
public function create(array $data)
{
$something = this->something->newInstance();
// Continue on with creation logic
}
Key is this method, newInstance().
I've used $newModel = $this->model and it's worked for me.