I have this table class:
class Songs extends Zend_Db_Table_Abstract
{
protected $_name = 'songs';
protected $_primary = 'song_id';
protected $_rowClass = 'Song';
}
And a class that extends the class above with some custom logic.
class Song extends Zend_Db_Table_Row_Abstract
{
protected function _insert()
{
print_r($this);
// $this does exist
}
protected function _update()
{
print_r($this);
//$this does not existing when updating a row, why not?
}
}
My problem is that when I'm inserting a new row I can use $this in my custom logic.
$row->save(); // $this exists in _insert()
But it doesn't exist when I'm trying to update a row.
$myRow->update($data, $where); // $this does not exists in _update()
Why does $this not exist when I want to do some custom logic before updating a row?
To update a row, you don't use:
$myRow->update($data, $where);
You use:
$myRow->save();
But trying to use update() on a row object should throw an exception.
So I'm guessing you're actually calling the update() function on the table object, and not the row object.
$songs = new Songs();
//...
$songs->update($data, $where);
At that point the row object is never even used, the query is simply generated from the $data array and the $where clause.
If you want to use the custom _update() method you would need to do something like:
$songs = new Songs();
$song = $songs->find($id)
//change some data
$song->save();
Of course is also perfectly valid to add custom logic at the table level, and should be noted while calling an update or insert from the table object does not use the row object, calling save() on the row object proxies the table object.
For example, from the Zend_Db_Table_Row _doInsert() function:
$this->_insert();
//...
$primaryKey = $this->_getTable()->insert($data);
So if you have custom logic that you want to use every time you update a row (whether you update from the table object or the row object), it should be put into the table object.
From the Zend_Db_Table_Row docs:
If you need to do custom logic in a specific table, and the custom logic must occur for every operation on that table, it may make more sense to implement your custom code in the insert(), update() and delete() methods of your Table class. However, sometimes it may be necessary to do custom logic in the Row class.
Related
I am trying to insert a two related objects via Eloquent and I get an Integrity constraint violation error on one of the tables. Here is what I am trying:
The representation of the relationship:
class DataParticipant extends Model
{
public function data_config()
{
return $this->hasOne('App\Models\DataConfig');
}
}
class DataConfig extends Model
{
public function data_participant()
{
return $this->belongsTo('App\Models\DataParticipant');
}
}
Then, I am trying the following:
$dataParticipant = new DataParticipant();
$dataParticipant->ip = // ip
$dataParticipant->code = // code
$dataParticipant->study_name = // study
// Prepare the config child.
$dataConfig = new DataConfig();
$dataConfig->config = // config via json_encode
// Store the child associated to the parent.
$dataParticipant->data_config()->save($dataConfig);
The error is on data_participant_id column, which belongs to the DataConfig model. My guess is that there is no id on which the two can be linked. I want to ask you:
How can I accomplish this without having to first save the DataParticipant then retrieve it from the database and then store the DataConfig on the resulted id?
First save the first model
$dataParticipant->save ();
Then save the second model
$dataParticipant->data_config()->save($dataConfig);
This won't retrieve the data from DB at all, and $dataParticipant id will be automatically set with the last insert id
This is my current query:
$cars = Cars::with('brand')->get();
$cars->map(function($cars){
$cars->fullName = $cars->brand->brand." ".$cars->name;
//other manipulation...
return $cars;
});
I want to manipulate my collection in the model so that I can run something like $cars = Cars::with('brand')->getWithBrand();
How can I do this, so I don't have to write map functions for every time I run the query?
In your particular example, you don't need to use map to modify the Collection at all. You can use an Eloquent accessor to define attributes on a Model that don't exist in the database. In your example, you would define the following method on your Cars model:
public function getFullNameAttribute($value)
{
// make sure brand exists first
if ($this->brand) {
return $this->brand->brand.' '.$this->name;
}
// default if brand doesn't exist
return $this->name;
}
By defining that function on your Model, that function will be called whenever you attempt to use the full_name attribute, as shown in the following code:
$car = Cars::with('brand')->first();
// this will echo the result of the getFullNameAttribute method
echo $car->full_name;
Edit
If you would also like this new attribute to automatically show up in your toArray() or toJson() output, you can add the attribute to the $appends property on your Cars model:
class Cars extends Model
{
protected $appends = ['full_name'];
public function getFullNameAttribute($value)
{
// make sure brand exists first
if ($this->brand) {
return $this->brand->brand.' '.$this->name;
}
// default if brand doesn't exist
return $this->name;
}
}
Be aware, however, that your custom attribute depends on a related object. So, if you do something that accidentally calls toArray(), toJson(), __toString(), etc on a Collection of Cars that has not eager loaded the brand relationship, this will cause the N+1 query issue.
For example:
// Bad: N+1 issue because each printed Car will execute a
// separate query to get its brand to output full_name.
echo Cars::get();
// Good: No N+1 issue because all brands are already loaded.
echo Cars::with('brand')->get();
I have a relationship set up between two models. The two models are set up like:
app/Character.php
public function characteristics() {
return $this->hasMany('App\Characteristics');
}
app/Characteristics.php
public function character() {
return $this->belongsTo('App\Character');
}
then in a controller I have a method to create a new character with a predetermined set of characteristics as follows:
app/Http/Controllers/CharacterController.php
public function newCharacter(Request $request) {
$character = new Character();
$characteristics = getCharacteristics($character->id);
// Save basic character stuff
$character->characteristics()->saveMany($characteristics);
$character->save();
}
The highlighted line is throwing an error because saveMany is not a function of Builder so how can I get the created character without having to do a find that would have to hit the database again?
I think you need to save the character model first, because if you're building a hasMany/belongsTo relationship your characteristics table must have a column for character_id, and when you do $character->characteristics()->saveMany($characteristics); Laravel will try to insert the ID from the parent model. And as per the code shared by you, you've just instantiated the model, by this point of time it doesn't have an ID associated with it. So you need to save the character model first and assuming the getCharacteristics() method is returning an array/collection of Characteristics Models, Following should work:
public function newCharacter(Request $request) {
$character = new Character();
$character->save();
$characteristics = getCharacteristics();
// Save basic character stuff
$character->characteristics()->saveMany($characteristics);
}
And to further clarify this for you, from the characteristics method in your Character model an instance of HasMany not a Builder is being returned, thus saveMany works here.
There is often the case where an certain eloquent model's relation is unset (i.e. in a books table, author_id is null) and thus calling something like $model->relation returns null.
E.g. say a Book model has an author() (hasOne) relation I might want to do
$author = Book::find(1)->author->name;
If Book 1 has no author set it will throw a "trying to get property of non object" error. Is there a way to avoid this and default to a blank Author so I'll always be able to call name on it regardless of whether the relation has been set for the specific model?
Essentially I want to avoid conditionals to check if $book->author is an actual Author before calling further methods/properties on it. It should default to a new Author instance if the relation isn't set.
I tried something like:
public function getAuthorAttribute($author)
{
return $author ?: new Author;
}
however this doesn't work; $author is being passed in as null, even if it's set on the model. Presumably because it's a relation rather than a direct property of a book. I'd need something like
public function getAuthorAttribute()
{
return $this->author()->first() ?: new Author;
}
which seems pretty inelegant and seems like it would override any eager loading resulting in poor performance.
Update
As of Laravel 5.3.23, there is now a built in way to accomplish this (at least for HasOne relationships). A withDefault() method was added to the HasOne relationship. In the case of your Book/Author example, your code would look like:
public function author() {
return $this->hasOne(Author::class)->withDefault();
}
This relationship will now return a fairly empty (keys are set) Author model if no record is found in the database. Additionally, you can pass in an array of attributes if you'd like to populate your empty model with some extra data, or you can pass in a Closure that returns what you'd like to have your default set to (doesn't have to be an Author model).
Until this makes it into the documentation one day, for more information you can check out the pull requests related to the change: 16198 and 16382.
At the time of this writing, this has only been implemented for the HasOne relationship. It may eventually migrate to the BelongsTo, MorphOne, and MorphTo relationships, but I can't say for sure.
Original
There's no built in way that I know of to do this, but there are a couple workarounds.
Using an Accessor
The problem with using an accessor, as you've found out, is that the $value passed to the accessor will always be null, since it is populated from the array of attributes on the model. This array of attributes does not include relationships, whether they're already loaded or not.
If you want to attempt to solve this with an accessor, you would just ignore whatever value is passed in, and check the relationship yourself.
public function getAuthorAttribute($value)
{
$key = 'author';
/**
* If the relationship is already loaded, get the value. Otherwise, attempt
* to load the value from the relationship method. This will also set the
* key in $this->relations so that subsequent calls will find the key.
*/
if (array_key_exists($key, $this->relations)) {
$value = $this->relations[$key];
} elseif (method_exists($this, $key)) {
$value = $this->getRelationshipFromMethod($key);
}
$value = $value ?: new Author();
/**
* This line is optional. Do you want to set the relationship value to be
* the new Author, or do you want to keep it null? Think of what you'd
* want in your toArray/toJson output...
*/
$this->setRelation($key, $value);
return $value;
}
Now, the problem with doing this in the accessor is that you need to define an accessor for every hasOne/belongsTo relationship on every model.
A second, smaller, issue is that the accessor is only used when accessing the attribute. So, for example, if you were to eager load the relationship, and then dd() or toArray/toJson the model, it would still show null for the relatioinship, instead of an empty Author.
Overriding Model Methods
A second option, instead of using attribute accessors, would be to override some methods on the Model. This solves both of the problems with using an attribute accessor.
You can create your own base Model class that extends the Laravel Model and overrides these methods, and then all of your other models will extend your base Model class, instead of Laravel's Model class.
To handle eager loaded relationships, you would need to override the setRelation() method. If using Laravel >= 5.2.30, this will also handle lazy loaded relationships. If using Laravel < 5.2.30, you will also need to override the getRelationshipFromMethod() method for lazy loaded relationships.
MyModel.php
class MyModel extends Model
{
/**
* Handle eager loaded relationships. Call chain:
* Model::with() => Builder::with(): sets builder eager loads
* Model::get() => Builder::get() => Builder::eagerLoadRelations() => Builder::loadRelation()
* =>Relation::initRelation() => Model::setRelation()
* =>Relation::match() =>Relation::matchOneOrMany() => Model::setRelation()
*/
public function setRelation($relation, $value)
{
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When attempting
* to set to null, override with a new instance of the expected model.
*/
if (is_null($value)) {
// set the value to a new instance of the related model
$value = $this->$relation()->getRelated()->newInstance();
}
$this->relations[$relation] = $value;
return $this;
}
/**
* This override is only needed in Laravel < 5.2.30. In Laravel
* >= 5.2.30, this method calls the setRelation method, which
* is already overridden and contains our logic above.
*
* Handle lazy loaded relationships. Call chain:
* Model::__get() => Model::getAttribute() => Model::getRelationshipFromMethod();
*/
protected function getRelationshipFromMethod($method)
{
$results = parent::getRelationshipFromMethod($method);
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When the
* result is null, override with a new instance of the related model.
*/
if (is_null($results)) {
$results = $this->$method()->getRelated()->newInstance();
}
return $this->relations[$method] = $results;
}
}
Book.php
class Book extends MyModel
{
//
}
I had the same problem in my project. In my views there's some rows that are accesing to dinamics properties from null relationships, but instead of returning an empty field, the app was thrwoing and exception.
I just added a foreach loop in my controller as a temporal solution that verifies in every value of the collection if the relationship is null. If this case is true, it assigns a new instance of the desire model to that value.
foreach ($shifts as $shift)
{
if (is_null($shift->productivity)) {
$shift->productivity = new Productivity();
}
}
This way when I access to $this->productivity->something in my view when the relationship is unset, I get a empty value instead of an exception without putting any logic in my views nor overriding methods.
Waiting for a better solution to do this automatically.
You can achieve this using model factories.
Define an author factory inside your ModelFactory.php
$factory->define(App\Author::class, function (Faker\Generator $faker) {
return [
'name' => $faker->firstName, //or null
'avatar' => $faker->imageUrl() //or null
];
});
add values for all the needed attributes I am using dummy values from Faker but you can use anything you want.
Then inside your book model you can return an instance of Author like this:
public function getAuthorAttribute($author)
{
return $author ?: factory(App\Author::class)->make();
}
I have a table called payments which contains a field called Vendor ZIP.
I have a table called 201502_postcodes and my "join" in this case is the postcode field in this table.
How do I return field values in this 201502_postcodes table using Eloquent?
My Models are;
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Payment extends Model {
public function postcodeExtract()
{
return $this->belongsTo('App\Models\PostcodeExtract', 'postcode', 'Vendor ZIP');
}
_
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PostcodeExtract extends Model {
protected $connection = 'postcodes';
public function scopeFromTable($query, $tableName)
{
return $query->from($tableName);
}
public function payment()
{
return $this->hasMany('App\Models\Payment', 'Vendor ZIP', 'postcode');
}
So, I have a scope on this model because the 201502 part of my table name is a variable (in that, a new one comes in every quarter).
In my controller... I have no idea what to put. I don't know how to get both scope and relationship to work. How can I write a query that will take a postcode/zip and output one of the fields from the (do I refer to them as "methods"?) postcode extract table?
It is not a duplicate of this question Laravel 4: Dynamic table names using setTable() because relationships are not involved or discussed on that question.
--- UPDATE ---
If I am to use getTable - would it go something like this...
class PostcodeExtract {
public function setTableByDate($selected_tablename)
{
$this->table = $selected_tablename;
// Return $this for method chaining
return $this;
}
public function getTable()
{
if (isset($this->table))
$this->setTableByDate($this->table);
return $this->table;
}
}
And then I would use it in my controller like;
$selected_tablename = 201502_postcode //created by some other controller
$postcode_extract = new PostcodeExtract;
$data = $postcode_extract->setTableByDate($selected_tablename)->get()->toArray();
The Carbon stuff isn't really relevant. I have a lookup to get those tablenames the fact the prefix with a date like value shouldn't mean it's treated like a date.
There are a couple of things going on here.
scopeFromTable() is redundant
Laravel employs magic methods to handle calls to undefined methods. Calling from() on the model will actually call from() on the models internal Query object (assuming you didn't define a method called 'from' on the model itself). It's worth reading the __call and __callStatic methods on the Model class.
relationships use getTable()
Another aspect of the Laravel is the concept of convention over configuration. This basically means that the framework assumes some things so that you don't have to define every detail. In regards to table naming convention, it will naturally use a table name derived from the class name.
// Uses table 'foos'
class Foo {}
There are a few ways to change this behavior. First, you can define a 'table' data member like this.
class Foo {
protected $table = 'bars';
}
If you need a more dynamic behavior, then you can redefine the getTable method.
class Foo {
public function getTable()
{
// return your special table name based on today's date
}
}
Ultimately the models and their relationships refer to getTable to figure out what the table names should be.
your use cases
If you only ever need to query the current table, then I would suggest redefining getTable.
If you need to query both current and past tables, then I suggest pairing a new method along side redefining getTable
class Foo {
public function setTableByDate(\DateTime $date)
{
$this->table = // generate table name from $date
// Return $this for method chaining
return $this;
}
public function getTable()
{
if (isset($this->table))
$this->setTableByDate(\Carbon\Carbon::now());
return $this->table;
}
}
With this in place, you don't have to worry about the table name in your controller or anywhere else unless you need to query past records.
setting the table by date per user
$foos = Foo::setTableByDate($user->some_date)->where(...)->get();