Laravel model relationships and model events - php

I am building a notification system at the moment, and the notifications get delivered via model events. Some of the notifications are dependent on things happening with the models relationships.
Example: I have a project model that has a one:many relationship with users,
public function projectmanager() {
return $this->belongsToMany('User', 'project_managers');
}
I am wanting to monitor for changes on this relationship in my project model events. At the moment, I am doing this by doing this following,
$dirtyAttributes = $project->getDirty();
foreach($dirtyAttributes as $attribute => $value) {
//Do something
}
This is run in the ::updating event of the model but only looks at the models attributes and not any of it's relational data, is it possible get old relational data and new relational data to compare and process?

You should be using an observer class for this.
This has already been covered fairly simply and well by this SO answer, although that answer uses a slightly older method where the class itself needs to call upon its observer. The documentation for the current version (5.3 as of this answer) recommends registering the observer in your application service provider, which in your example would look similar to:
<?php
namespace App\Providers;
use App\Project;
use App\Observers\ProjectObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Project::observe(ProjectObserver::class);
}
}
For evaluating the differences between the new model values and the old values still in the relational DB, Laravel provides methods for both: getDirty() and getOriginal().
So your observer would look something like:
<?php
namespace App\Observers;
use App\Project;
class ProjectObserver
{
/**
* Listen to the Project updating event.
*
* #param Project $project
* #return void
*/
public function updating(Project $project)
{
$dirtyAttributes = $project->getDirty();
$originalAttributes = $project->getOriginal();
// loop over one and compare/process against the other
foreach ($dirtyAttributes as $attribute => $value) {
// do something with the equivalent entry in $originalAttributes
}
}
}

Related

Laravel Queries: Adding custom feature like Soft Deletes

All of my tables have a column called isTest. What I want is to be able to set a switch so that my code will either include all records in my queries or [more importantly] exclude all records where isTest is true.
I imagine the code will work similarly to Soft Deletes and include sql code similar to: AND (isTest != TRUE) to SQL generated by Eloquent and the Query Builder.
I am not really familiar with Eloquent events, but I have found this question which might be the right place to start, but I am hoping for guidance before I start down that path. Also, that has no info about Query Builder. If someone has done something similar I would love some advice.
You are looking for Global scopes, you can add a custom scope which will check the isTest value.
<?php
// Your custom scope
namespace App\Scopes;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class IsTestScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param \Illuminate\Database\Eloquent\Model $model
* #return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->where('isTest', true);
}
}
// Your model
namespace App;
use App\Scopes\IsTestScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
// Check for certain criteria, like environment
if (App::environment('local')) {
// The environment is local
static::addGlobalScope(new IsTestScope);
}
}
}
When you have a lot of models, you want to make a trait of this code so you dont have to duplicate it all the time. Like how SoftDeletes work.
See docs for more info https://laravel.com/docs/5.8/eloquent#global-scopes

Binding to Laravel IoC instead of instantiating again and again

In my app I have a service called "LogService" to log events and other items. I basically need to use this on every controller to log events by users. Instead of having to instantiate this service in each controller, I had two thoughts for accomplishing this.
Option 1: Bind the service into the IoC and then resolve it that way
Option 2: Make a master class with the service in it and then extend it for other classes so they come with the service already bound
I have questions for each of these methods:
Option 1: Is this even possible? If so, would it just be with "App::make()" that it would be called? That way doesn't seem to play too well with IDE's
Option 2: I have done this kind of thing in the past but PHPStorm does not seem to recognize the service from the parent object because it is instantiated by "App::make()" and not through the regular dependency injection.
What would be the best course of action?
Thanks!
You can have it both ways, I think the neatest way would be:
1) Have an interface that describes your class, let's call it LogServiceInterface
2) Create a Service Provider that instantiates your class, like so:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class LoggerServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
$this->app->bind(LogServiceInterface::class, function($app)
{
return new LogService();
});
}
}
3) Register this service provider in config/app.ph file:
'providers' => [
// Other Service Providers
App\Providers\LoggerServiceProvider::class,
],
4) Now, in controller you can request the instance of something that implements LoggerServiceInterface straight in the constructor:
(Some controller):
<?php namespace App\Http\Controllers;
use Illuminate\Routing\Controller;
use App\Repositories\OrderRepository;
class OrdersController extends Controller {
/**
* The logger service.
* #var LoggerServiceInterface $loggerService
*/
protected $loggerService;
/**
* Create a controller instance.
*
* #param OrderRepository $orders
* #return void
*/
public function __construct(LoggerServiceInterface $loggerService)
{
$this->loggerService = $loggerService;
}
/**
* Show all of the orders.
*
* #return Response
*/
public function index()
{
// $this->loggerService will be an instance of your LoggerService class that
// is instantiated in your service provider
}
}
This way, you have got an easy way to quickly change the implementation of your service, moreover, Phpstorm can handle this very easily.
You will still be able to use app()->make() to obtain an instance of your service.
This, however, will not be automatically picked up by Phpstorm. But you can help it to understand that, all you need to do is to use #var annotation, see:
/**
* #var LoggerServiceInterface $logger
*/
$logger = app()->make(LoggerServiceInterface::class);
That way, Phpstorm will know what to expect from that $logger object.

Pass data in an included view with Laravel 5.6

Using Laravel 5.6, I'm trying to get the number of received links a logged-in user may have in my application.
public function getReceivedLinksCount() {
return $count = App\Link::where([
['recipient_id', Auth::id()],
['sender_id', '!=', Auth::id()]
])->count();
}
So far, so good. Question is not about that piece of code, but where I can use it. I'd like to display this counter on the navigation bar of the website (Facebook's style) which is in my header.blade.php which is included in every page.
I'd like to do it with a clean code, of course. It seems like I need to use View Composers but I'm not sure it's the right way to do it, and not sure on how the code is supposed to look.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
view()->composer('layouts.header', function ($view) {
$view->with('variable_name', \App\Path-To-Your-Model::something());
});
}
You can share a value across all views by using View::share(), see docs
For example
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$linksCount = Link::getReceivedLinksCount();
View::share('linksCount', $linksCount);
}
...
}
This works well if you want to set the value everywhere. Personally, I would set the value in the constructor of a 'BaseController' that gets extended by other controllers. This makes the code more discoverable because most people would expect view values to be set in a controller. And it's also a bit more flexible if you plan on having a section of your app that doesn't require that value to be computed.

Laravel DependencyInjection is this "okay" to do?

To handle some logic of my application I created a Service in App/Services/CarsService.php.
I injected this service in my controller through DependencyInjection like so:
CarsController.php
<?php
namespace App\Http\Controllers;
use App\Services\CarsService;
class CarsController extends Controller
{
/** #var CarsService $carsService */
private $carsService;
/**
* Create a new controller instance.
*
* #param CarsService $carsService
*/
public function __construct(CarsService $carsService)
{
$this->carsService = $carsService;
}
So for example when I want to query all the cars with some parameters provided by the user I do something like this in one of my controller methods:
$cars = $this->carsService->getCars($brand, $type);
This keeps my controller clean and my logic is seperated in a Service, seems pretty good and clean to me.
But my question is actually if this is bad practice to do in Laravel? I imagine that there might be a more "Laravel-y" way to handle this.
You don't want to use a service for that. Inject and use model if you're using Eloquent. Or use repository if you're using Query Builder, raw queries or API. For example:
public function __construct(Car $car)
{
$this->car = $car;
}
$this->car->getByBrandAndType($brand, $type);
If you're asking about is using IoC container a good or a bad practice, it's definitely a good tool to use in any app.

What is the right way to synchronize Laravel model with an external API data sources?

I would like to hear your tips or solutions to this problem. Even more helpful would be links to some working!! open source projects which implements some of these mechanisms.
For example i want to synchronize all github's gist with Laravel model. I think about two form.
Option 1
Write separate classes for example Importer and call it's method from controllers and commands and interact with Model.
Option 2
Put data extracting logic (api calls) inside Laravel's model hierarchy (traits,interfaces and so) and within artisan's command let's say synchronize:all loop through all model and check for any changes and update each row.
example
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Sample\Http\Client;
class Gist extends Model
{
/**
* Example of fetching new gists
*/
public static function getNew($gistId = '6cad326836d38bd3a7ae')
{
$resposne = Client::request('GET', 'https://api.github.com/gists/'.$gistId)
->response()
->json();
$Gist = new static($response);
$Gist->save();
return $Gist;
}
/**
* Get list of all gist from external source and ... who knows
*/
public function listAllGistFromApi()
{
$resposne = Client::request('GET', 'https://api.github.com/gists/')
->response()
->json();
return $response;
}
/**
* Example of updating existing gist
*/
public function updateFromApi()
{
$gistId = $this->id; // primary key also api uid
$resposne = Client::request('GET', 'https://api.github.com/gists/'.$gistId)
->response()
->json();
return $this->setAttributes($response)->save(); // simplified logic
}
}
Your solutions...
You can use observers, what it does? it keeps an eye on the model changes, there you can perform your Apis to the out data source, take a look here how to apply observes in Laravel

Categories