Returning virtual columns in Phalcon models - php

I have a model leads_contents_interactions for the (simplified) table:
CREATE TABLE `leads_contents_interactions` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`lead_content_id` bigint(20) unsigned DEFAULT NULL,
`created_on` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=59 DEFAULT CHARSET=utf8;
I would like to select these and in addition to the id, lead_content_id, and created_on columns, I would also like it to return a column is_new where that is something like this:
SELECT
id,
lead_content_id,
created_on,
IF(created_on > DATE_SUB(NOW(),INTERVAL 1 DAY), 1, 0) AS is_new
FROM leads_contents_interactions;
Now I am aware I can do this with PHQL, but the leads_contents_interactions would ideally not be queried directly, I want this extra column to be returned when it is queried naturally like:
$leads = $user->getRelated(
'leads',
array(
'Lead.deleted_by IS NULL',
'limit'=>1000
)
);
foreach($leads as $lead) {
foreach($lead->interactions as $interaction) {
echo $interaction->id."\t".$interaction->is_new.PHP_EOL;
}
}
Model for Lead (simplified)
class Lead extends PersendlyModelAbstract {
public function initialize() {
// A lead has multiple interactions, `contents`, through the weak entity `leads_contents`
$this->hasManyToMany(
'id',
'LeadsContents',
'lead_id',
'id',
'LeadsContentsInteractions',
'lead_content_id',
array('alias' => 'interactions')
);
}
}
Model for LeadsContents (simplified)
class LeadsContents extends PersendlyModelAbstract {
public function initialize() {
$this->belongsTo('lead_id', 'Lead', 'id', array('alias' => 'lead'));
$this->belongsTo('content_id', 'Content', 'id', array('alias' => 'content'));
$this->hasMany('id', 'LeadsContentsInteractions', 'lead_content_id');
}
}
Model for LeadsContentsInteractions (simplified)
class LeadsContentsInteractions extends PersendlyModelAbstract {
public function initialize() {
$this->belongsTo('lead_content_id', 'LeadsContents', 'id', array('alias' => 'lead_content'));
}
}

If you are wanting to add a column that doesn't exist on the table, but exists as a business rule (created_on > DATE_SUB(NOW(),INTERVAL 1 DAY), 1, 0) then you need to add that rule in the afterFetch method of the model itself:
http://docs.phalconphp.com/en/latest/reference/models.html#initializing-preparing-fetched-records
class LeadsContentsInteractions extends PersendlyModelAbstract
{
public $isNew;
public function afterFetch()
{
$this->isNew = INSERT BUSINESS LOGIC HERE
}
}
It should however be noted, that if you then use the method toArray() on the record set, that it will only use the columns that exist on the table itself.
http://forum.phalconphp.com/discussion/498/afterfetch-

Overriding toArray() Method for Virtual Fields.
In response to what David Duncan said:
It should however be noted, that if you then use the method toArray()
on the record set, that it will only use the columns that exist on the
table itself.
And to circumvent such Phalcon 'limitation', I created the following method override.
Step 1
Basically, create a BaseModel.php and put the next code there.
/**
* Method override.
*
* This method is inherited from Model::toArray()
* https://docs.phalconphp.com/en/3.2/api/Phalcon_Mvc_Model
*
* We override it here to circumvent a Phalcon limitation:
* https://stackoverflow.com/a/27626808/466395
*
* Basically, the limitation consists that, when one adds 'virtual fields' to a model (for example,
* by way of callback methods like afterFetch()), then using toArray() on that model only returns
* the fields in the database table but not the virtual fields.
*
* #access public
* #param array $columns As per the Model::toArray() method.
* #return array The data of the model, including any custom virtual fields.
*/
public function toArray($columns = null) {
// calls the regular toArray() method
$data = parent::toArray($columns);
// then gets the model's virtual fields, if any
$virtual_fields = [];
if (!empty($this->list_virtual_fields)) {
// iterates, to get the virtual field's name, value, and getter
foreach ($this->list_virtual_fields as $name) {
$getter_name = 'get' . \Phalcon\Text::camelize($name);
$virtual_fields[$name] = $this->{$getter_name}();
}
}
// merges the model's database data with its virtual fields
$data = array_merge($data, $virtual_fields);
return $data;
}
Step 2
Then, in any of your app models, define the list of virtual fields that will be included in the method override above. For example:
public $list_virtual_fields = [
'status_label'
];
You should also define class properties, setters, and getters for those virtual fields. Just an example:
protected $status_label;
public function getStatusLabel() {
return $this->status_label;
}
public function setStatusLabel(string $status_label) {
$this->status_label = $status_label;
return $this;
}
Step 3
Finally, set the value of virtual fields throughout your app. An example:
public function afterFetch() {
$this->setStatusLabel('pending');
}
Note that my code uses getters and setters. You could change that if you wish.

Related

Laravel Eloquent. Same relation, different type of return

I think there is basically no difference between both eloquent relations but work differently.. I can't find what I missed..
There are three model Concept, Attribute, Status. Those models have its own MySQL table concepts, attributes, statuses.
Concepts Table
id : integer(10) unsigned primary
name, description, .. : string(various) nullable
Attributes Table
id : integer(10) unsigned primary
concept_id : integer(10) unsigned foreign on concepts.id
Statuses Table
id : integer(10) unsigned primary
parent_id : integer(10) unsigned foreign on concepts.id
To define eloquent relation, I sorted things like below.
Concept has many Attribute
Concept has many Status
So naturally, I write each relation bidirectionally on each model.
Inside model Concept.php file..
public function attributes()
{
return $this->hasMany('App\Attribute');
}
public function statuses()
{
return $this->hasMany('App\Status');
}
Inside Attribute.php file..
public function concept()
{
return $this->belongsTo('App\Concept');
}
Inside Status.php file..
public function parent()
{
return $this->belongsTo('App\Concept');
}
Problem happens when I use method $concept->attributes.
On Concept.php file..
public function inherit()
{
// Copy basic concept data. (name, description, etc.)
$children = $this->replicate();
$children->name .= ' (Inherited)';
$children->inherit_parent_id = $this->id;
$children->inherit_origin_id = $this->inherit_origin_id ?? $this->id;
$children->save();
// Inherit Statuses
foreach( $this->statuses as $status ) {
$status->inheritTo( $children );
};
// Inherit Attributes
foreach( $this->attributes as $attribute ) {
$attribute->inheritTo( $children );
};
return $children;
}
As far as I know, when the first foreach was executes, $this->statuses returns Collection object. However, on the second foreach block, $this->attributes returns Concept object. So I try $this->attributes()->get() then it works fine.
Why those two foreach block works differently?
$this->attributes is a inherited property of Model class. So it shows these instead the relation.
If you rename your function to refer to the relation, it should work out for you.
Inside model Concept.php file (just an example name)
public function rel_attributes()
{
return $this->hasMany('App\Attribute');
}
And you refer to the relation like that:
// Inherit Attributes
foreach( $this->rel_attributes as $attribute ) {
$attribute->inheritTo( $children );
};
I agree with Philip's answer. You need to always define relations in eloquent that don't override the default variables.

laravel 5.4 polymorphic relation - eager read via query scope

I have a polymorphic relation where a table 'map_nodes' has a one-to-many to 'system_constants'.
I can't seem to use a query scope to eager load system_constants. No errors, just no records.
Here's my models. I've only included one model 'map_nodes':
namespace App\Modules\Olab\Models;
class SystemConstants extends BaseModel {
// Database:
// id INT(11) PK
// imageable_id INT(11)
// imageable_type VARCHAR(45)
// value BLOB
public function imageable() {
return $this->morphTo();
}
}
class MapNodes extends BaseModel {
public function scopeConstants( $query ) {
$query->with( [ 'SystemConstants' => function ( $query ) {
$query->select( 'system_constants.id',
'system_constants.value' );
} ] );
}
public function SystemConstants() {
return $this->morphMany('App\Modules\Olab\Models\SystemConstants', 'imageable');
}
}
This is what I have in system_constants table:
id, imageable_id, imageable_type, value
'1904', '25786', 'Nodes', <blob>
Note that I do have a Relation::morphMap defined, which allows me to use 'Nodes' in imageable_type.
This is what I'm running:
// returns collection of system_constant records
$temp = MapNodes::find(25786)->SystemConstants;
// doesn't work. get map_nodes record, but no eager load of system_constants
$temp = MapNodes::find(25786)->Constants()->first();
I have sql logging turned on and I see the correct SQL for both above queries. I can take that exact query in an SQL tool (with param substitution) and it works - it returns records:
select `system_constants`.`id`, `system_constants`.`value` from
`system_constants` where `system_constants`.`imageable_id` in (?) and
`system_constants`.`imageable_type` = ? - a:2:{i:0;i:25786;i:1;s:5:"Nodes";}
Any help appreciated.
I'm not sure, but this seems to have worked. I just removed the query scope and did an explicit 'with'.
$temp = MapNodes::with('SystemConstants')->find(25786);

Laravel - Eloquent - Dynamically defined relationship

Is it possible to set a model's relationship dynamically? For example, I have model Page, and I want to add relationship banners() to it without actually changing its file? So does something like this exist:
Page::createRelationship('banners', function(){
$this->hasMany('banners');
});
Or something similar? As they are fetched using the magic methods anyway, perhaps I can add the relationship dynamically?
Thanks!
I've added a package for this i-rocky/eloquent-dynamic-relation
In case anyone still looking for a solution , here is one. If you think it's a bad idea, let me know.
trait HasDynamicRelation
{
/**
* Store the relations
*
* #var array
*/
private static $dynamic_relations = [];
/**
* Add a new relation
*
* #param $name
* #param $closure
*/
public static function addDynamicRelation($name, $closure)
{
static::$dynamic_relations[$name] = $closure;
}
/**
* Determine if a relation exists in dynamic relationships list
*
* #param $name
*
* #return bool
*/
public static function hasDynamicRelation($name)
{
return array_key_exists($name, static::$dynamic_relations);
}
/**
* If the key exists in relations then
* return call to relation or else
* return the call to the parent
*
* #param $name
*
* #return mixed
*/
public function __get($name)
{
if (static::hasDynamicRelation($name)) {
// check the cache first
if ($this->relationLoaded($name)) {
return $this->relations[$name];
}
// load the relationship
return $this->getRelationshipFromMethod($name);
}
return parent::__get($name);
}
/**
* If the method exists in relations then
* return the relation or else
* return the call to the parent
*
* #param $name
* #param $arguments
*
* #return mixed
*/
public function __call($name, $arguments)
{
if (static::hasDynamicRelation($name)) {
return call_user_func(static::$dynamic_relations[$name], $this);
}
return parent::__call($name, $arguments);
}
}
Add this trait in your model as following
class MyModel extends Model {
use HasDynamicRelation;
}
Now you can use the following method to add new relationships
MyModel::addDynamicRelation('some_relation', function(MyModel $model) {
return $model->hasMany(SomeRelatedModel::class);
});
As of laravel 7, dynamic relationship is officially supported. You can use the Model::resolveRelationUsing() method.
https://laravel.com/docs/7.x/eloquent-relationships#dynamic-relationships
you can use macro call for your dynamic relation like this:
you should write this code in your service provider boot method.
\Illuminate\Database\Eloquent\Builder::macro('yourRelation', function () {
return $this->getModel()->belongsTo('class');
});
You have to have something in mind, an Eloquent relationship is a model of a relational database relatioship (i.e. MySQL).
So, I came with two approaches.
The good
If you want to achieve a full-featured Eloquent relationship with indexes and foreing keys in the database, you probably want to alter the SQL tables dynamically.
For example, supossing you have all your models created and don't want to create them dynamically, you only have to alter the Page table, add a new field called "banner_id", index it and reference to "banner_id" field on Banner table.
Then you have to write down and support for the RDBMS you will work with.
After that, you may want to include support for migrations. If it's the case, you may store in the database these table alterations for further rollbacks.
Now, for the Eloquent support part, you may look at Eloquent Model Class.
See that, for each kind of relation, you have a subyacent model (all can be found here, which is in fact what you are returning in relatioship methods:
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$localKey = $localKey ?: $this->getKeyName();
return new HasMany($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
So you have to define a method in your model that accepts the type of relation and the model, creates a new HasMany (in case hasMany was the desired relationship) instance, and then returns it.
It's little bit complicated, and so you can use:
The easy
You can create a intermediate model (i.e. PageRelationship) that stores all the relationships between Page and other Models. A possible table schema could be:
+-------------+---------+------------------+-------------+
| relation_id | page_id | foreign_model_id | model_class |
+-------------+---------+------------------+-------------+
| 1 | 2 | 225 | Banner |
| 2 | 2 | 223 | Banner |
| 3 | 2 | 12 | Button |
+-------------+---------+------------------+-------------+
Then you can retrieve all dynamically relative models to a given Page. The problem here is that you don't actually have any real RDBMS relation between Models and Pages, so you may have to make multiple and heavy queries for loading related Models, and, what's worse, you have to manage yourself database consistency (i.e., deleting or updating the "225" Banner should also remove or update the row in page_relationship_table). Reverse relationships will be a headache too.
Conclusion
If the project is big, it depends on that, and you can't make a model that implements other models via inheritance or so, you should use the good approach. Otherwise, you should rethink you app design and then decide to choose or not second approach.
Just in case anyone is looking for a Laravel 8 answer:
Let's say I define my relationships in a single method of my model:
public function relationships()
{
return [
'user' => $this->belongsTo(User::class, 'user_id'),
];
}
Now, in my app service provider, I can use the resolveRelationUsing method. I've done this by iterating through the models folder and checking all models which contain the aforementioned method:
foreach ((new Filesystem)->allFiles(app_path('Models')) as $file) {
$namespace = 'App\\Models\\' . str_replace(['/', '.php'], ['\\', ''], $file->getRelativePathname());
$class = app($namespace);
if (method_exists($class, 'relationships')) {
foreach ($class->relationships() as $key => $relationship) {
$class->resolveRelationUsing($key, function () use ($class, $key) {
return $class->relationships()[$key];
});
}
}
}

Call to undefined method Gallery::newQuery() Laravel

I have 3 tables
products
pages
gallery - two foreign keys page_id, product_id
(are the foreign keys supposed to be mapped together UNIQUE, why?)
<?php
class Gallery extends Eloquent {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'gallery';
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
public static $rules = array(
// 'email'=>'required|email|unique:users',
// 'password'=>'required|alpha_num|between:6,12|confirmed',
// 'password_confirmation'=>'required|alpha_num|between:6,12'
);
public function pages() {
return $this->belongsTo('Pages', 'page_id');
}
public function products() {
return $this->belongsTo('Products', 'products_id');
}
}
Pages Model
<?php
class Pages extends Eloquent {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'pages';
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
public static $rules = array(
'title'=>'required',
'body'=>'required'
// 'email'=>'required|email|unique:users',
// 'password'=>'required|alpha_num|between:6,12|confirmed',
// 'password_confirmation'=>'required|alpha_num|between:6,12'
);
public function gallery() {
return $this->hasOne('Gallery');
}
}
Products Model
<?php
class Products extends Eloquent {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'products';
// public $timestamps = false;
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
public static $rules = array(
'title'=>'required',
'body'=>'required'
// 'email'=>'required|email|unique:users',
// 'password'=>'required|alpha_num|between:6,12|confirmed',
// 'password_confirmation'=>'required|alpha_num|between:6,12'
);
public function gallery()
{
return $this->hasOne('Gallery');
}
}
I'm doing this in PagesController:
$page = Pages::find($page_id);
$page->gallery throws this error Call to undefined method Gallery::newQuery();
I created the Gallery table and foreign keys using migrations. Why do i have to create relationships between the models, these relationships are defined in the database? (le noob question)
2.Read a lot about what could be the cause of this and it's not namespacing.
Been stuck on this for 3 days now, any help is greatly appreciated.
I think is has something to do with my model relationships.
=============================== UPDATE =========================
Okay, so i did $gallery = new Gallery; in my PageController, and this seems to worky
But now i get this funky error:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (spaggo.gallery, CONSTRAINT gallery_product_id_foreign FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE) (SQL: insert into gallery (page_id, updated_at, created_at) values (6, 2015-06-21 15:24:51, 2015-06-21 15:24:51))
First, What's your Laravel version? It actually might be due to namespacing in spite of your assumption.
Secondly, as a convention I recommend you name your models singular such as Product and Page.
To answer your other question: Yes, your database may contain constrains for these relations, but unfortunately Eloquent ORM can't make use of them. You're still required to define these relations at the model level.
Shouldn't it be extends Model for all Laravel models (unless of course it's some old syntax) and that Model must be declared via use statement: use Illuminate\Database\Eloquent\Model;.
I know first hand that if I forget to add Model parent class to my models then Call to undefined method App\SomeClass::newQuery error follows.

Laravel Removing Pivot data in many to many relationship

Not sure if I set this up correctly. In Laravel I'm creating two models with a many-to-may relationship
The models are Item and Tags. Each one contains a belongsTo to the other.
When I run a query like so:
Item::with('tags')->get();
It returns the collection of items, with each item containing a tags collection. However the each tag in the collection also contains pivot data which I don't need. Here it is in json format:
[{
"id":"49",
"slug":"test",
"order":"0","tags":[
{"id":"3","name":"Blah","pivot":{"item_id":"49","tag_id":"3"}},
{"id":"13","name":"Moo","pivot":{"item_id":"49","tag_id":"13"}}
]
}]
Is there anyway to prevent this data from getting at
you can just add the name of the field in the hidden part in your model like this:
protected $hidden = ['pivot'];
that's it , it works fine with me.
You have asked and you shall receive your answer. But first a few words to sum up the comment section. I personally don't know why you would want / need to do this. I understand if you want to hide it from the output but not selecting it from the DB really has no real benefit. Sure, less data will be transferred and the DB server has a tiny tiny bit less work to do, but you won't notice that in any way.
However it is possible. It's not very pretty though, since you have to override the belongsToMany class.
First, the new relation class:
class BelongsToManyPivotless extends BelongsToMany {
/**
* Hydrate the pivot table relationship on the models.
*
* #param array $models
* #return void
*/
protected function hydratePivotRelation(array $models)
{
// do nothing
}
/**
* Get the pivot columns for the relation.
*
* #return array
*/
protected function getAliasedPivotColumns()
{
return array();
}
}
As you can see this class is overriding two methods. hydratePivotRelation would normally create the pivot model and fill it with data. getAliasedPivotColumns would return an array of all columns to select from the pivot table.
Now we need to get this integrated into our model. I suggest you use a BaseModel class for this but it also works in the model directly.
class BaseModel extends Eloquent {
public function belongsToManyPivotless($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null){
if (is_null($relation))
{
$relation = $this->getBelongsToManyCaller();
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$otherKey = $otherKey ?: $instance->getForeignKey();
if (is_null($table))
{
$table = $this->joiningTable($related);
}
$query = $instance->newQuery();
return new BelongsToManyPivotless($query, $this, $table, $foreignKey, $otherKey, $relation);
}
}
I edited the comments out for brevity but otherwise the method is just like belongsToMany from Illuminate\Database\Eloquent\Model. Of course except the relation class that gets created. Here we use our own BelongsToManyPivotless.
And finally, this is how you use it:
class Item extends BaseModel {
public function tags(){
return $this->belongsToManyPivotless('Tag');
}
}
If you want to remove pivot data then you can use as protected $hidden = ['pivot']; #Amine_Dev suggested, so i have used it but it was not working for me,
but the problem really was that i was using it in wrong model so i want to give more detail in it that where to use it, so you guys don't struggle with the problem which i have struggled.
So if you are fetching the data as :
Item::with('tags')->get();
then you have to assign pivot to hidden array like below
But keep in mind that you have to define it in Tag model not in Item model
class Tag extends Model {
protected $hidden = ['pivot'];
}
Two possible ways to do this
1. using makeHidden method on resulting model
$items = Item::with('tags')->get();
return $items->makeHidden(['pivot_col1', 'pivot_col2']...)
2. using array_column function of PHP
$items = Item::with('tags')->get()->toArray();
return array_column($items, 'tags');

Categories