Laravel Model factories, one to many relationship without database access - laravel

For my tests (Laravel 6) I am trying to use factories with make(), in order to bypass the DB operations just creating a new instance of the model.
Now, I am having an headache to make things work with my one to many relationship.
A skeleton of my code just to have an overview:
class Fine extends Model {
// Other things here
public function articles()
{
return $this->hasMany('App\Model\Article');
}
}
where Article does not have any other relationships.
Now, I would like to create a Fine factory to create an instance in which I can parse $fine->articles, all without any database interaction.
Here's the basic Fine factory that works (without using articles):
$factory->define(Fine::class, function (Faker $faker) {
return [
"id" => 10,
Fine::FIELDONE => 'xxx',
Fine::FIELDTWO => 'yyy',
];
});
that I use with  $fine = factory(Fine::class)->make(); .
Now I need to "prefill" articles. I have tried with  hydrate() but it does not work.
I have tried with afterMaking() with different combinations of code:
$factory->afterMaking(Fine::class, function (Fine $fine, Faker $faker) {
$articles = factory(\App\Model\Article::class, 3)->make([\App\Model\Article::FINE_ID => $fine->id]);
// TEST WITH SAVEMANY: $fine->saveMany(factory(\App\Model\Article::class, 10)->make());
// SAME WITH CREATEMANY
// TEST WITH HYDRATE: $multa->articles()->hydrate([$article]);
// $multa->load('articles');
});
Just to make a point I have tried different roads but, I admit, I am a bit lost.
Basically I end up with the classical
Illuminate\Database\QueryException: SQLSTATE[42S02]: Base table or view not found
error, so I am still accessing the database.
Now, I am starting to think that factories are a proper choice only if at the end one is going to store data on the database (I am sure that saveMany() is going to work if I use create() and not make()).
Can you please give me an hint on how to accomplish this task? If factories are not the right way to do it I am more than open to change my choice, too.
Thanks in advance!
Lorenzo

You can set this information on a non existing model instance if you really have to:
$fine->setRelation(
'articles',
factory(\App\Model\Article::class, 3)->make()
)
It depends what you plan on doing with this model and its relationship since there are no IDs set so they are not really related in that way.

Related

n-n relation in Backpackforlaravel: doesn't delete corresponding items

I'm having troubles with a n-n relationship in my Backpackforlaravel app. I have Registrations and Sessions, the setup in the model looks like this:
Registration:
public function sessions()
{
return $this->belongsToMany(\App\Models\Session::class);
}
Session:
public function registrations()
{
return $this->belongsToMany(\App\Models\Registration::class);
}
So the setup seems to be fine, and I also have a registration_session table in my database.
What I want to achieve is, that whenever a Session gets deleted, I want to delete all the entries in the registration_session table, but also all the Registrations. I thought the deletion of the entries in the registration_session table maybe is a standard, but it didn't delete them when I deleted a session. In order to achieve both, I did the following in the destroy function of my SessionCrudController:
public function destroy($id)
{
$this->crud->hasAccessOrFail('delete');
foreach ($this->crud->getCurrentEntry()->registrations as $registration) {
$registration->delete();
}
DB::table('registration_session')->where('session_id', $id)->delete(); //really required?
return $this->crud->delete($id);
}
I have the feeling that I'm doing things way more complicated than I should, so I would appreciate any recommendations.
Update: this is how I store the data initially in the table...the information is coming from a form, so it's not added in a CRUD panel:
DB::table('registration_session')->insert([
'registration_id' => $reg->id,
'session_id' => $sesId
]);
By default, Backpack or Laravel don't concern themselves with deleting related entries, because they assume your Database layer will handle it. With most DBMSs you can specify such rules, and Laravel makes it easy to do so in their migrations. Take a look at foreign key constraints in the Laravel docs, it's pretty easy to build your migration in such a way that any time one item gets deleted, the related items will to, using "cascade":
$table->foreignId('user_id')
->constrained()
->onUpdate('cascade')
->onDelete('cascade');

laravel/elequent - models and relations

I trying to learn laravel and to do some tests/demo apps. I've struggling now with laravel/eloquent tables relations. And I need advice.
I have 3 models [Application, Term, AppState] and their tables applications[id, terms_id, appStates_id, and other cols ], terms[id, startDate, endDate, ...], app_states[id, caption]
Application.php
public function term()
{
return $this->belongsTo('App\Term');
}
public function appState()
{
return $this->belongsTo('App\AppState');
}
in Term.php and AppState.php i have:
public function applications()
{
return $this->hasMany('App\Application');
}
How I can get let's say "caption"/"startDay"+"endDate" in blade for each application? I can get their ids $app->terms_id/$app->appStates_id in foreach loop, but i want get caption value from app_states table.
Has to be this relations also specified in migrations? In some tuts is mentioned, that is not needed in case i want to handle it only in laravel.
Thanks for advice
You can access a model's relationship values by calling the relationship method like a property.
$application = Application::find(1);
$application->term->startDate;
$application->term->endDate;
$application->appState->caption;
Also your relationship with AppState is wrong, since your foreign key doesn't follow a snake_case typing, you'll need to provide the appropriate key for it
public function appState()
{
return $this->belongsTo('App\AppState', 'appStates_id');
}
You might also want to check terms_id as well since the model name (Term) is singular but the foreign key is plural.
Has to be this relations also specified in migrations? In some tuts is mentioned, that is not needed in case i want to handle it only in laravel.
Well, yes, you don't need to if Laravel will only be the one accessing that database. But if any cases in the near future you decide to migrate to a different framework or use the same database in another application, it's better to include these relationships in the migration. Also a database administrator would probably cringe if you don't.
So provided your relationships are correctly setup, you can access them anywhere you have an instance of that model.
So for example, lets say you have passed a collection of applications to your view ($apps):
#foreach($apps as $app)
{{ $app->term->startDate }}
{{ $app->term->endDate }}
{{ $app->appState->caption }}
#endforeach
Important Note: We are accessing the Eloquent relationship using ->appState rather than ->appState(). The later is actually accessing a Query Builder instance and has some more advanced use cases

Overriding Laravel get and first methods

I need to override above mentioned methods to skip some database records. Using where is not an option since I would have to use it every single time as there are records in database that I do not need most of the time and I am not allowed to delete them from DB. Here is my attempt of doing this:
class SomeTable extends BaseModel {
public static function first() {
$query = static::query();
$data = $query->first();
if($data && $data->type == 'migration_type') return null;
return $data;
}
public static function get() {
$query = static::query();
$data = $query->get();
foreach($data as $key => $item) {
if($item->type == 'migration_type') unset($data[$key]);
}
return $data;
}
}
The problem with this code is that it works only when direct called on model. If I am using some other functions, like where, before get or first methods, it just skips my overridden method.
What would be the right way to do this and should I put this code within model?
My question is not duplicate as in the answer from mentioned question it is said:
all queries made from Models extending your CustomModel will get this new methods
And I need to override those two functions only for specific model, not for each one in application as not all tables have type column. That's the reason why I have written them within model class.
I need to override above mentioned methods to skip some database records.
Consider a global query scope on the model.
https://laravel.com/docs/5.8/eloquent#global-scopes
Global scopes allow you to add constraints to all queries for a given model. Laravel's own soft delete functionality utilizes global scopes to only pull "non-deleted" models from the database. Writing your own global scopes can provide a convenient, easy way to make sure every query for a given model receives certain constraints.
The issue here is that the where() method on the model returns a QueryBuilder instance where get() will return a Collection instance.
You should be able to override collection's default methods by adding a macro in it's place and can be done like so...
Collection::macro('toUpper', function () {
return $this->map(function ($value) {
return Str::upper($value);
});
});
Extending the query builder instance is not so easy but a good tutorial exists here and involves overriding the application's default connection class, which is not great when it comes to future upgrades.
Because after calling where you're dealing with the database builder and theses methods inside your model aren't being called .. about the issue you might overcome it by using select instead of first directly so will deal with the builder ..
example:
SomeTable::select('col1','col2')->take(1)->get();
another thing overriding these kind of methods is not a good idea if you're working with other developer on the same project.
good luck

Fetching data from multiple tables in laravel5

I have two tables posts and sharedposts. sharedposts table has columns "user_id", "updated_at" and "post_id" where as posts table has 'id', 'title' and 'body'. I know about the relations to link both tables. All i want to do is to display each post ordered by 'updated_at' from both the tables. Please help with both query builder and eloquent way. Thanku
The code is not tested, but you can try this:
Post::whereHas('sharedpost',function($query){
$query->orderBy('updated_at');
})->get()
Maybe a bit different approach if there won't be to much of the updates of the models? This way I suppose the read should be faster (somebody please correct me if I'm wrong here)
Add another field to the table by creating new migration and adding
$table->timestamp('last_updated');
Then using Events update the field whenever the Post or SharedPost is updated by adding this to AppServiceProvider:
public function boot()
{
Post::saving(function ($post) {
$post->last_updated = Carbon::now();
});
}
I'm not sure if this will work though and it would be appreciated if you returned a feedback as I'm lazy to try it out myself at the moment :). Worse case scenario you might need to change the method as:
public function boot()
{
SharedPost::saved(function ($sharedpost) {
$sharedpost->post->last_updated = Carbon::now();
$sharedpost->save();
});
}
or you could check out Observers if you don't want the code in the AppServiceProvider. Haven't used it but it seems straightforward.
Then you could simply Post::orderBy('last_updated')->get();
This way you will have one or two more inputs to your database but I think (and guys please correct me if I'm wrong) the get() should be faster. Keep in mind I'm far from a MySQL expert.
And in the end, you can make it even faster by saving this data to Cache using Redis.

Laravel - Retrieve the inverse of a many-to-many polymorphic relation (with pagination)

after some digging I still could not find any solid way to retrieve the inverse of a many-to-many polymorphic relation that allows mixed models results.
Please consider the following:
I have several models that can be "tagged". While it is trivial to retrieve for example $item->tags, $article->tags and the inverse with $tag->articles and $tag->items I have no easy way to do something like $tag->taggables to return both articles and items in the same collection. Things get even bumpier as I need to use pagination/simple pagination to the query.
I have tried a few workarounds but the best I could put together still looks crappy and limited. Basically:
I queried the DB once per "taggable";
put all in a single big collection;
passed the collection to a phpleague/fractal transformer (my API uses it) that returns different json values depending on the parsed models.
The limits of this approach is that building a pagination is a nightmare and fractal "include" options can't be used out of the box.
Can anyone help me? I'm currently using Laravel 5.1.
There is not much magic in my current code. Faking and simplifying it to make it short:
From the api controller:
$tag = Tag::findOrDie($tid);
$articles = $tag->cms_articles()->get();
$categories = $tag->cms_categories()->get();
$items = $tag->items()->simplePaginate($itemsperpage);
$taggables = Collection::make($articles)->merge($categories);
// Push items one by one as pagination would dirt the collection struct.
foreach ($items as $item) {
$taggables->push($item);
}
return $this->respondWithCollection($taggables, new TaggableTransformer);
Note: using simplePaginate() is there only because I would like all articles and categories to be shown on first page load while the number of items are so many that need pagination.
From the Transformer class:
public function transform($taggable)
{
switch (get_class($taggable)) {
case 'App\Item':
$transformer = new ItemTransformer;
break;
case 'App\CmsArticle':
$transformer = new CmsArticleTransformer;
break;
case 'App\CmsCategory':
$transformer = new CmsCategoryTransformer;
break;
}
return $transformer->transform($taggable);
}
Please consider that the other transformers are simply returning arrays of data about the models they correlate with. If you use Fractal you would easily spot that nested "included" models would not be applied.
Nothing fancy for the Tag model:
class Tag extends Model
{
protected $morphClass = 'Tag';
protected $fillable = array('name', 'language_id');
public function cms_articles() {
return $this->morphedByMany('App\CmsArticle', 'taggable');
}
public function cms_categories() {
return $this->morphedByMany('App\CmsCategory', 'taggable');
}
public function items() {
return $this->morphedByMany('App\Item', 'taggable');
}
// Would love something like this to return inverse relation!! :'(
public function taggables() {
return $this->morphTo();
}
}
I am also considering the option to do 3 separate calls to the API to retrieve articles, categories and items in three steps. While in this particular scenario this might make sense after all, I would still need to deal with this particular inverse relation headache with another part of my project: notifications. In this particular case, notifications would have to relate to many different actions/models and I would have to retrieve them all in batches (paginated) and sorted by model creation date...
Hope this all makes sense. I wonder if a completely different approach to the whole inverse "polymorphic" matter would help.
Kind regards,
Federico
Ah yes. I was down your path not all that long ago. I had the same nightmare of dealing with resolving the inverse of the relationship of polymorphic relationships.
Unfortunately polymorphic relationships haven't been given much attention in the Laravel ecosystem. From afar they look like unicorns and rainbows but soon you're fighting things like this.
Can you post an example of a $thing->taggable for a better picture? Think it may be solvable with a dynamic trait + accessor magic.

Resources