Just wondering if there is an easy way to auto-populate an object’s related fields on an instance-by-instance basis rather than globally in the config file, or, for the entire class.
I’d like to include all related models for a single instance without chaining a ton of include_related() functions.
Something like this would be nice:
$x = new Model();
$x->include_all_related();
Thought I'd have to get my hands dirty in the core. For whatever reason, it didn't occur to me that I could access the $has_many and $has_one arrays.
Solution was simple:
class Model extends Datamapper{
var $has_one = array('foo', 'bar', 'baz');
var $table = 'models';
function __construct($id = NULL){
parent::__construct($id);
}
function include_all_related(){
foreach($this->has_one as $h){
$this->include_related($h['class']);
}
return $this;
}
}
You might wonder why I'm using the class key in the $h variable. Under the hood, Datamapper ORM keeps track of some other keys too as part of a bigger array. If you call print_r($h), you can see them. The class key keeps track of foo, bar and baz.
You can enable it on the relation definition, if you want is on a particular instance, you need to change the relation configuration of that instance at runtime.
Related
I am currently moving over from symfony to laravel, it's quite a bit different when it comes to the database. So i have a basic model, i'm just going to use an example:
class Test extends Model
{
use HasFactory;
}
All good, i have a migration and the table created. However, i don't like this:
$test = new Test();
$test->my_field = 'hello';
$test->save();
I don't like it because it's having to use a magic __set() to create the parameter, if i define the parameter in my model like this:
class Test extends Model
{
use HasFactory;
public ?string $my_field;
}
I get database errors when it tries to insert when i define the params like this. Why is that? It's doing the same thing as __set() but i'm actually physically defining them, which in my opinion is a better way to code it as my IDE can typehint and it's just nicer to follow the program knowing what params are there.
What's the reason for it inserting when i don't define them, and not when i do? From my actual table which is bookings , has a field booking_ref:
General error: 1364 Field 'booking_ref' doesn't have a default value (SQL: insert into booking_reviews (updated_at, created_at) values (2021-12-13 14:13:08, 2021-12-13 14:13:08))
This happens when i define the $booking_ref param on the model, but if i take it out and rely on the __set() method it works fine. Doesn't make any sense to me right now.
I think this is a reasonable enough misunderstanding to be useful to future visitors, so I want to try to explain what's going on with some pseudo-code and some references to the current source code.
You are correct that when setting a property on a Laravel model, that is a column in the DB, internally Laravel is using the PHP magic method __set.
What this does is allow you to 1) set properties directly instead of calling some kind of setter function, and 2) interact with your table columns without needing the boilerplate of column definitions in your model.
Where the assumptions go wrong is with what __set is doing. __set does not have to simply set an actual property with the same name. __set is just a method you may implement to do whatever you want. What you assumption implies is that it's doing something like this:
public function __set($key, $value)
{
$this->{$key} = $value;
}
However, you can do whatever you want with the $key and $value passed to the magic method.
What Laravel does is call another method defined in the HasAttributes trait - setAttribute.
public function __set($key, $value)
{
$this->setAttribute($key, $value);
}
setAttribute does a few extra things, but most importantly it adds the key/value pair to Model property $this->attributes[].
To hopefully help this difference make sense, here is what the two __set methods would yield with a basic example:
$model->my_column = 'value';
// 1st example
/**
* {
* public $my_column = 'value';
* }
*/
// Laravel way
/**
* {
* protected $attributes= ['my_column => 'value'];
* }
*/
I won't go through both saving and updating since they're very similar, but to show how this is used, we can look at the save method, which calls performInsert and after a few more calls makes it's way back to the attributes property to determine what to actually insert into the query.
Summary
Laravel does not use custom model properties when deciding what column/values to add to queries.
This is why when you create custom mutators, you interact with the attributes property just like Laravel does internally.
Anytime you introduce "magic" into code, you have some tradeoffs. In this case, that tradeoff is slightly less clarity with what database columns are actually available. However, like I mentioned in comments, there are other solutions to make models more IDE friendly like Laravel IDE helper.
Is there any possible way to lazy load a custom attribute on a Laravel model without loading it every time by using the appends property? I am looking for something akin to way that you can lazy load Eloquent relationships.
For instance, given this accessor method on a model:
public function getFooAttribute(){
return 'bar';
}
I would love to be able to do something like this:
$model = MyModel::all();
$model->loadAttribute('foo');
This question is not the same thing as Add a custom attribute to a Laravel / Eloquent model on load? because that wants to load a custom attribute on every model load - I am looking to lazy load the attribute only when specified.
I suppose I could assign a property to the model instance with the same name as the custom attribute, but this has the performance downside of calling the accessor method twice, might have unintended side effects if that accessor affects class properties, and just feels dirty.
$model = MyModel::all();
$model->foo = $model->foo;
Does anyone have a better way of handling this?
Is this for serialization? You could use the append() method on the Model instance:
$model = MyModel::all();
$model->append('foo');
The append method can also take an array as a parameter.
Something like this should work...
public function loadAttribute($name) {
$method = sprintf('get%sAttribute', ucwords($name));
$this->attributes[$name] = $this->$method();
}
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
In my controller I have:
$Locations = Locations::where(something);
$Locations->get()->toArray(true);
And inside the model:
function toArray($include_all = false) {
var_dump($include_all);
}
The include all variable is false, although the function gets called.
Is there a reason why it's doing that ?
I want to call a custom toArray because I have more oneToMany relations with different structures that I want to change (some of them are serialized for example)
Thank you
You can use Illuminate\Support\Collection methods such as map() and filter() to modify the collection and at the end of that call toArray() method.
It would be fairly easy to overwrite, however I think there is some confusion that should be cleared up first.
First, the toArray() method you are calling in this case is on the Collection which is the object which is returned when you use get() on your model.
With that said, you can add the following to your Location model to return a custom collection...
public function newCollection(array $models = [])
{
return new CustomCollection($models);
}
Then you write the new CustomCollection class with appropriate namespaces just to make sure it gets auto loaded fine, have it extend \Illuminate\Database\Eloquent\Collection and then you can proceed to override the toArray method.
However, it feels like you randomly selected this toArray() as a proper candidate to perform your logic just because you are already using it. You should think about creating a new function which calls $this->toArray() to grab the results and modify them as you need and return that.
If you need this same functionality on other models, just keep adding that newCollection method where needed.
This is also in the docs as well, might be worth checking out...
https://laravel.com/docs/5.2/eloquent-collections#custom-collections
I'd like to include some additional functions in my Doctrine 2 entities to contain code that I'm going to have to run quite frequently. For example:
User - has many Posts
Post - has a single user
I already have a function $user->getPosts(), but this returns all of my posts. I'm looking to write a $user->getActivePosts(), which would be like:
$user->getPosts()->where('active = true') //if this were possible
or:
$em->getRepository('Posts')->findBy(array('user'=>$user,'active'=>true)) //if this were more convenient
As far as I can tell, there's no way to get back to the entity manager though the Entity itself, so my only option would be
class User {
function getActivePosts() {
$all_posts = $this->getPosts();
$active_posts = new ArrayCollection();
foreach ($all_posts as $post) {
if ($post->getActive()) {
$active_posts->add($post);
}
}
return $active_posts;
}
However, this requires me to load ALL posts into my entity manager, when I really only want a small subset of them, and it requires me to do filtering in PHP, when it would be much more appropriate to do so in the SQL layer. Is there any way to accomplish what I'm looking to do inside the Entity, or do I have to create code outside of it?
I think you should implement the method on the PostRepository rather than on the entity model.
I try to keep all model related logic in the repositories behind "domain specific" methods. That way if you change the way you represent whether a post is active or not, you only have to change the implementation of a single method instead of having to find all the active = true statements scattered around in your application or making changes in an "unrelated" entity model.
Something like this
PostRepository extends EntityRepository {
public function findActiveByUser($user){
// whatever it takes to get the active posts
}
}