Laravel query builder macro on loaded models - php

I have developed a package to enable search method on Eloquent models with JSON.
The engine currently works in a way that a query is appended for supported parameters depending on values provided.
Builder::macro('search', function (Request $request) {
/**
* #var $this Builder
*/
$searcher = new Searcher($this, $request);
$searcher->search();
return $this;
});
Which in turn runs the following:
/**
* Perform the search
*
* #throws Exceptions\SearchException
*/
public function search(): void
{
$this->appendQueries();
Log::info('[Search] SQL: ' . $this->builder->toSql());
}
/**
* Append all queries from registered parameters
*
* #throws Exceptions\SearchException
*/
protected function appendQueries(): void
{
foreach ($this->requestParametersConfig->registered as $parameter) {
$requestParameter = $this->createRequestParameter($parameter);
$requestParameter->appendQuery();
}
}
protected function createRequestParameter($parameter): AbstractParameter
{
return new $parameter($this->request, $this->builder, $this->modelConfig);
}
Request parameters are registered within configuration file and point to request parameter classes to have everything modular.
Now this works great if used on a something like index endpoint or a custom search endpoint, but I would like to extend the functionality to show as well. The issue being here that with route model binding, the model is already loaded when I fetch it.
Let's take relationships for example. I have a request parameter which when used loads relationships on a given model:
www.example.com/api/contacts?relations=(phones)
will load phones relations on Contact model by doing this:
/**
* Append the query to Eloquent builder
* #throws SearchException
*/
public function appendQuery(): void
{
$arguments = $this->getArguments();
$this->builder->with($arguments);
}
But when I do a ->with on already loaded model followed by ->get(), I just get list of all models with loaded relation.
Is there a way to "tap into" already loaded model with query builder, or is it at this point already done deal, and I should pick up the ID on show route instead of resolving it, and then assemble the query?

After a lot of research of Laravel inner workings, what I asked initially isn't possible.
At the point where you call the method to interact with DB in some way, a query builder instance is made and depending on the method you called on the original model, the query is formed. After execution there is nothing to tap into as the object no longer exists.
Also, from the logical point of view, I have concluded that search makes no sense on already loaded models, so I did a concept switch there and disabled the package for already loaded models.

Related

Stopping 'with' attribute recursion in Laravel Eloquent model

Assume two Eloquent models in Laravel - user and location.
User:
class User extends Model
{
/**
* Relations that should be automatically loaded.
*
* #var array
*/
public $with = [
'locations'
];
/**
* Returns relation to the locations table.
*/
public function locations() {
return $this->belongsTo('App\Entities\Location');
}
}
Location:
class Location extends Model
{
/**
* Relations that should be automatically loaded.
*
* #var array
*/
public $with = [
'users'
];
/**
* Returns relation to the users table.
*/
public function users() {
return $this->hasMany('App\User');
}
}
As you can see, they both have the public attribute with - because as these models are driving an API, I want the users to always be returned with their location, and locations to always be returned with a list of their users.
However, as I thought might happen, this has led to the request timing out due to (I think) recursion. In other words, the location loads its users, and they load their locations, which then load their users, and so on.
Is there any way in Laravel to force it not to recurse into the same model that is doing the 'initial loading'?
I should mention that user also has another relation, profile, that I want to load in every query. So if, for example, there's a way to simply kill with eager-loading after a certain 'depth', that won't do it.
EDIT: I am aware that I can use User::with('location')->find(); to eager-load the relations, however this solution requires me to use that extra code everywhere I am retrieving users. I am looking for a solution that will let me always eager-load the relationships automatically.
It makes a infinite loop of loading for example you called a User now it is loading Location but when Location loaded then it again called User it has been loaded and the same loading process is going on so it makes a loop which is not even breaking. that why it is give a error
you can do like this
User::with('location')->find(..) // or get or any
You can resolve this problem, just put other model in belongsTo method.
Create new empty JustLocation model extends Location model.
Set protected $with = []; in JustLocation model
Change return $this->belongsTo('App\Entities\Location'); to return $this->belongsTo('App\Entities\JustLocation'); in the User model.

Symfony where clause on the entity level

I've got two entities that are linked together by a one-to-many relationship and I'm using soft deletes on both entities. Because I'm using soft deletes however, reading data is a little bit more tricky because I need to check if the deleted flag is set to false before reading it out.
The basic setup of the entities are:
class Division extends MasterData {
...
/**
* #var Asset
*
* #ORM\OneToMany(targetEntity="Asset", mappedBy="division")
*/
private $assets;
public function __construct() {
$this->assets = new ArrayCollection();
}
public function getAssets() {
return $this->assets;
}
public function addAssets(Asset $asset) {
$this->assets[] = $asset;
return $this;
}
...
}
class Asset extends MasterData {
...
/**
* #var Division
*
* #ORM\ManyToOne(targetEntity="Division", inversedBy="assets")
*/
private $division;
...
}
class MasterData {
/**
* #ORM\Column(name="deleted", type="boolean", options={"default":0})
*/
protected $deleted;
public function __construct() {
$this->deleted = 0;
}
...
}
These are only snippets of the entities, not the entire thing.
When I am in the controller for a Division, I'd like to pull a list of all the Assets that are related to that division and are not marked as deleted. I can see a couple ways of doing this.
An easy solution would be to create a custom repository to handle the pull of data. This however would provide a limitation when I would like to further filter data (using findBy() for example).
A second solution would be to alter the getAssets() function in the Division entity to only return assets that are not deleted. This however means that I'm pulling all of the data from the database, then filtering it out post which is very inefficient.
Ideally, I'm looking for a way to alter the definition in the entity itself to add a where clause for the asset itself so that way the filtering is happening in the entity removing the needs for custom repositories and a more efficient option. Similar as to how I can define #ORM\OrderBy() in the annotations, is there a way to similar to this that lets me filter out deleted assets pre-execution and without a custom repository?
Thanks in advance :)
Doctrine does not support conditional associations in mapping. To achive this behavior you can use Criteria API in the entity methods. And yes, in this case all data will be fetched from DB before applying condition.
But Doctrine (>=2.2) supports Filters. This feature allows to add some SQL to the conditional clauses of all queries. Soft-deletes can be implemented through this feature.
The DoctrineExtensions library already has this functionality (SoftDeletable, based on Filters API).
Also, many don't recommend to use soft-deletes (1, 2).

How can I append an Eloquent property from a related model without getting the model's contents?

I have 2 eloquent models:
EloquentUser
and
SharedEvents
They are both related by user_id
I'm attempting to set up and appends attribute in the SharedEvents model that will append the full_name of the user with whom the event has been shared.
For the sake of readability, I'm only including the appends components of my class
class SharedEvents extends Model {
protected $appends = ['fullName'];
/**
* #return BelongsTo
*/
public function user() : BelongsTo {
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id');
}
/**
* #return mixed
*/
public function getFullNameAttribute(){
return $this->user->user_full_name;
}
Unfortunately when I run this I'm getting back both the full name and the entire user model when I only want the full name.
Is there a way to avoid attaching the content of the user model?
It feels like you're trying to make columns from your EloquentUser model first class citizens in your SharedEvent model. You're getting close, but consider...
When working with relationships, this is a good way to be explicit:
Assuming user_full_name is an accessor on your User model:
// EloquentUser.php
// This will automatically add your accessor to every query
// as long as you select the columns the accessor is made
// up of
protected $appends = ['user_full_name'];
/**
* User Full Name Accessor
* #return string
*/
public function getUserFullNameAttribute()
{
return $this->first_name . ' ' . $this->last_name;
}
// SharedEvent.php
/**
* A SharedEvent belongs to an Eloquent User
* #return BelongsTo
*/
public function user() : BelongsTo
{
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id');
}
// Somewhere in your controller or wherever you want to access the data
$sharedEvents = SharedEvent::with(['user' => function ($query) {
$query->select('user_id', 'first_name', 'last_name');
}])->where(...)->get(['shared_with_id', ...other columns]); // you must select the related column here
This should get you the closest to what you want, but there are a couple of things you should know:
If user_full_name is an accessor, you need to select all of the columns that make up that accessor (as I mention above)
You must select the related keys (user_id in EloquentUser and shared_with_id in SharedEvent)
The $appends is necessary in EloquentUser here because you can't directly add an accessor to your sub query inside the closure.
Try to get comfortable with using a closure as the 2nd argument in your relationships. It's the best way to really be precise as to which columns you're selecting when you're loading relationships — Eloquent makes it really easy to be lazy and just do:
SharedEvent::with('user')->get();
which as you've see will just do a select * on both SharedEvent and your user relationship.
Another thing I've noticed when working with complex queries that use relationships is that you can quickly reach a point where it feels like you're fighting the framework. That's often a sign to consider simplifying ot just using raw SQL. Eloquent is powerful, but is just another tool in your programming tool belt. Remember that you have other tools at your disposal.
I was running into the same problem, and my first idea was to explicitly hide the (entire) associated model when fetching SharedEvent.
In your case, this would look like:
class SharedEvents extends Model {
protected $appends = ['fullName'];
protected $hidden = ['user'];
...
}
I actually decided against this since I am returning a lot of other attached models, so I decided on attaching the entire user, but I tested it and it works.
FYI, I'm not sure if the discrepancy is due to an older version of Laravel, but Eloquent actually expects $appends attributes to be in snake-case (https://laravel.com/docs/5.6/eloquent-serialization#appending-values-to-json):
Note that attribute names are typically referenced in "snake case", even though the accessor is defined using "camel case"
Can you try this?
public function user() : BelongsTo {
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id')->select('fullName','user_id');
}
After doing some research and experimentation, it doesn't look like there's a way to do this with Eloquent. The solution I ended up going with unfortunately is running an additional query.
/**
* This is inefficient, but I could not find a way to get the full name
* attribute without including all of user.
*
* #return string
*/
public function getFullNameAttribute(){
return EloquentUser::select('user_full_name')
->where('user_id', $this->shared_with_id)
->first()
->user_full_name;
}

Controller methods in Laravel

I was developing codeigniter app for last 2 years and since i started my mvc pattern styling from codeigniter itself, now i'm not a heavy command line user so at first i did had some learning curve with codeigniter but i crossed it and started developing apps with code igniter because we don't need to configure a lot and everything was inside one zip file but now as unfortunately codeigniter is dead and i'm just one person in my team i have to rely on other third party tools which are trusted by others so i decided to switch to laravel, now at starting it was way way tough to migrate from codeigniter because composer and every other stuff, but somehow i crossed that too, but i'm now confused with routing and other stuff and i've tried many tutorials but i'm still not able to see how can i migrate from application where i'm managing students, where they can change email, change phone number updated stuff, in codeigniter it was easy but i don't how to approach this stuff in routing of laravel, now this question sounds way to dumb for community who is already working on laravel but if you see from my point of view it is going to affect my bread and butter. This is how i use to approach in codeigniter
class Student extends CI_Controller{
// usual piece of code of constructor
function update_email()
{
// piece of code to update email
}
}
but now with laravel routing system and all i've no idea how to approach this a resource controller looks like this
<?php
class StudentController extends \BaseController {
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index()
{
}
/**
* Show the form for creating a new resource.
*
* #return Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store()
{
//
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* #param int $id
* #return Response
*/
public function update($id)
{
//
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return Response
*/
public function destroy($id)
{
//
}
}
now every part is okay but how can i approach things where i've to update only email address or only phone number and stuff
Actually, in your question you have a RESTful controller which could be confusing for you at this time because you are new to Laravel and used CI, so probably you are very so much used with automating URL mapping without route. In this case, for ease, I suggest you to use a plain controller and that is almost same thing that you've did in CI but here in Laravel you have to declare route for every action. So, just create two routes like these:
Rouite::get('something/edit/{id}', array('uses' => 'StudentController#edit', 'as' => 'edit.record'));
Rouite::post('something/update/{id}', array('uses' => 'StudentController#update', 'as' => 'update.record'));
Then create a class/Controller and declare these methods:
class StudentController extends baseController {
// URL: http://example.com/something/edit/10 and it'll listen to GET request
public function edit($id) {
// $record = Load the record from database using the $id/10
// retuen View::make('editform')->with('record', $record);
}
// URL: http://example.com/something/update/10 and it'll listen to POST request
public function update($id) {
// Update the record using the $id/10
}
}
In your form, you need to use http://example.com/something/update/10 as action and you may use route('update.record') or url('something/update/10') to generate the action in the form. Read more on Laravel Documentation.
What you have created (or maybe rather generated) here is called Restful controller. It is some 'standard way' to manage CRUD actions (create/read/update/delete). However there is no need to do it this way. You can add in your controller whatever methods you want and not use Restful Controllers at all. That's your choice.
You can create in your function new method
function updateEmail()
{
// do here whatever you want
}
and in your routes.php file add new route:
Route::match(array('GET', 'POST'), '/changegemail', 'StudentController#updateEmail');
Now you can write your code for this method and it will work.

Execute code when Eloquent model is retrieved from database

I have an Eloquent model. Whenever it is retrieved from the database I would like to check whether a condition is fulfilled and set a model attribute if this is the case.
EDIT: I initially thought that the restoring event would be the right place to put the relevant logic, but as Tyler Crompton points out below, restoring is fired before a soft-deleted record is restored.
You have two valid options:
You can subclass \Illuminate\Datebase\Eloquent\Model to add such an event.
You can modify your copy of \Illuminate\Datebase\Eloquent\Model to add this (and possibly send an (unsolicited) pull request to Laravel on GitHub). According to Issue 1685, it looks as though they do not want it.
If I were you, I'd go with the first option and this is how I'd do it:
<?php namespace \Illuminate\Database\Eloquent;
abstract class LoadingModel extends Model {
/**
* Register a loaded model event with the dispatcher.
*
* #param \Closure|string $callback
* #return void
*/
public static function loaded($callback)
{
static::registerModelEvent('loaded', $callback);
}
/**
* Get the observable event names.
*
* #return array
*/
public function getObservableEvents()
{
return array_merge(parent::getObservableEvents(), array('loaded'));
}
/**
* Create a new model instance that is existing.
*
* #param array $attributes
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function newFromBuilder($attributes = array())
{
$instance = parent::newFromBuilder($attributes);
$instance->fireModelEvent('loaded', false);
return $instance;
}
}
Just make sure the models in question subclass from LoadingModule. I have confirmed this to work as I found a great use case for it. Older versions of PHP returned MySQL values as strings. Normally, PHP will silently cast these to their respective numeric types in numeric operations. However, converting to JSON is not considered a numeric operation. The JSON values are represented as strings. This can cause problems for clients of my API. So I added a loaded event to my models to convert values to the correct type.
You could do this on the way in, or the way out. It seems like you wanted it stored in the database, so you could use mutators.
class Foo extends Eloquent {
public function setBAttribute($value)
{
if ($this->attributes['a'] == $this->attributes['b']) {
$this->attributes['b'] = 1;
}
}
}
When ever B is set, it will check against A, and store 1 in B.
Side note: Note the B between set and attribute
I think this is the best option you can use.
The retrieved event will fire when an existing model is retrieved from the database. for example, if you have a User model in your application you must define code like below in User Model.
self::retrieved(function (self $model){
//all your code here
});

Categories