I cannot access joined table with PHPActiveRecord/Twig. Here is simplified code. It has two models (Code and User), and each code belongs to one user, so I want to list codes with the name of code's author.
php
// model
class Code extends ActiveRecord\Model {
static $belongs_to = array(
array('user'),
);
}
class User extends ActiveRecord\Model {
static $has_many = array(
array('code'),
);
}
// controller
$codes = Code::all(array('include' => 'user'));
var_dump($codes); //-> successfully displayed codes list and their authors
$this->twig->display('codelist.twig', $codes);
template.twig
{% for code in codes %}
{{ code.name }} //-> successfully displayed code's name
{{ code.user.name }} //-> failed to output user's name with error
{% endfor %}
// error:
// An exception has been thrown during the rendering of a template ("Call to undefined method: user") in "inc/template.twig" at line **.
I saw this page:
http://twig.sensiolabs.org/doc/templates.html
Implementation
For convenience sake foo.bar does the following things on the PHP
layer:
check if foo is an array and bar a valid element; if not, and if foo
is an object, check that bar is a valid property; if not, and if foo
is an object, check that bar is a valid method (even if bar is the
constructor - use __construct() instead); if not, and if foo is an
object, check that getBar is a valid method; if not, and if foo is an
object, check that isBar is a valid method; if not, return a null
value. foo['bar'] on the other hand only works with PHP arrays:
check if foo is an array and bar a valid element; if not, return a
null value.
Although I can access user attribute via $codes[0]->user, why can't I access user attribute in twig templates file?
Thanks to greut, I solved the problem. I replaced function __isset in lib/Model.php in PHPActiveRecord.
/**
* Determines if an attribute exists for this {#link Model}.
*
* #param string $attribute_name
* #return boolean
*/
public function __isset($name)
{
// check for getter
if (method_exists($this, "get_$name"))
{
$name = "get_$name";
$value = $this->$name();
return $value;
}
return $this->read_attribute($name);
}
https://github.com/kla/php-activerecord/issues/156
Related
I'm using Laravel 9 and I have a request can contains :
Parameter called SEASON the value can be an array or null
so SEASON parameter can be an array and can be also null
Parameter called EXPIRY can be an array and can be also null
I have two classes one for the SEASON feature and the other class for EXPIRY both they extends from Repository. and both have a method called execute that return an array
abstract class Repository
{
abstract public function execute(): array;
}
class Expiry extends Repository
{
public function execute()
{
return ['The Request contain Expiry Parameter, and seasonal behaviours is done'];
}
}
class Season extends Repository
{
public function execute()
{
return ['The Request contain Season Parameter, and expiry behaviours is done'];
}
}
I would like to call execute method of Season class if my request contains SEASON, or call the execute method of expiry if my request contains Expiry. OR Call both of them and merge the execute return of execute in one array so I can have as result.
['The Request contain Expiry Parameter, and seasonal behaviours is done', 'The Request contain Expiry Parameter, and expiry behaviours is done']
That's what I tried inside my controller :
public function bootstrap($data)
{
$parseTopics = Helper::parseTopicsRequest();
$basicProgram = new BasicProgramRepository();
$seasonalProgram = new SeasonalProgramRepository($parseTopics['SEASONAL']);
$object = count($parseTopics['SEASONAL']) ? $seasonalProgram : $basicProgram;
// Polymorphism
return $object->execute();
}
Question 1 :
I'm not sure if I should use this way or something like to fix my need:
$employe = new Program(new BasicProgramRepository());
Expected Result :
The expected result depends on if I have season parameter and expiry. What I want to achieve is to use different behaviours ( execute method )
if you want to achieve Polymorphism method, it will be better creating repository or something only for managing that logic.
here is sample.
class SampleRepository
{
/**
* repository instance value
*
* #var string[] | null
*/
private $sampleArray; // maybe here is SEASON or EXPIRY or null
/**
* constructor
*
* #param string[] | null $sampleArray
*/
public function __construct($sampleArray)
{
$this->sampleArray = $sampleArray;
}
/**
* execute like class interface role
*
* #return array
*/
public function execute()
{
return (!$this->sampleArray) ? [] : $this->getResult();
}
/**
* get result
*
* #return array
*/
private function getResult()
{
// maybe pattern will be better to manage another class or trait.
$pattern = [
"SEASON" => new Season(),
"EXPIRY" => new Expiry()
];
return collect($this->sampleArray)->map(function($itemKey){
$requestClass = data_get($pattern,$itemKey);
if (!$requestClass){ // here is space you don't expect class or canIt find correct class
return ["something wrong"];
}
return $requestClass->execute();
})->flatten();
}
}
and you can call like this.
$sampleRepository = new SampleRepository($sampleValue); // expect string[] or null like ["SEASON"],["SEASON","EXPIRY"],null
$result = $sampleRepository->execute(); // [string] or [string,string] or []
this approach is only what your parameter is secified value.
if your return result is almost same both of Season class and Expiry class, it will be better to manage on trait. (that is $pattern on sample code)
try some.
I read comments,so following..
For example, it prefers to be only getting result of getResult().
so, some pattern and so many logics shouldn't be written on getResult();
If you use trait, this is sample.
first, you need to create managing behaviors class.
Behavior.php
<?php
namespace App\Repositories;
class Behavior
{
use Behavior\BehaviorTrait;
// if you need to add another pattern, you can add trait here.
}
and then, you need to create Behavior directory at same level place.
you move that directory, you create trait file like this.
<?php
namespace App\Repositories\Behavior;
trait BehaviorTrait
{
public static function findAccessibleClass(string $itemKey)
{
return data_get([
"SEASON" => new Season(),
"EXPIRY" => new Expiry()
],$itemKey);
}
}
findAccessibleClass() method has responsible of finding correct class.
then, you call this method like this.
private function getResult()
{
return collect($this->sampleArray)->map(function($itemKey){
$requestClass = Behavior::findAccessibleClass($itemKey); // fix here.
if (!$requestClass){ // here is space you don't expect class or canIt find correct class
return ["something wrong"];
}
return $requestClass->execute();
})->flatten();
}
if your code is so much in getResult(), you will be better to separate code for responsible.
To create Behavior trait, getResult don't need to have responsible of behavior logic. it will be easy testing or fixable in short.
hope well.
I wonder what is the good practices :
Let's say I have 2 entities, ManyToOne. Both are ApiResources, and both have an Output DTO. So Both have a transformer.
<?php
/**
* #ORM\Entity
* #ApiResource(
* output="Dto\Foo"
* )
*/
class Foo
{
private int $id;
/**
* #ORM\ManyToOne(targetEntity="Bar")
*/
private Bar $bar;
}
Problem is, when I transform the entity Foo into a DTO Foo, I want to hydrate it with a Bar DTO, not a Bar entity. But since I hydrate it with from an entity, I have a Bar entity. Later in the process, the Bar entity is replaced by a Bar DTO, ApiPlateform is working, but my mental problem is : the bar property type is modified over time (Moreover it can't be Typehinted). Seems dirty to me, isn't it ?
Illustration:
the Transofmer
<?php
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
class FooEntityToFooDToTransormer implements DataTransformerInterface
{
public function transform($object, string $to, array $context = [])
{
return new FooDto($object);
// maybe there is a better way to hydrate FooDto, by getting directly a BarDto here ?
}
}
The DTO :
<?php
namespace Dto;
class Foo
{
public int $id;
// problem is I cant typehint here
public $bar;
public function __construct(FooEntity $fooEntity)
{
$this->id = $fooEntity->getId();
$this->bar = $fooEntity->getBar(); // <-- return a Bar entity, transformed later by ApiPlatform into a Bar DTO.
}
}
It there a way or a good practice to proper hydrate a DTO from an entity, especially about relations ?
Edit :
I actually prefer not Typehint $bar as its normalization (so its transformation) should be handled by ApiPlateform. But then, circular references are not handeled (memory limit) and I don't really know why (probably confusion between object and DTO).
I don't think my FooTransformer should know how to transform Bar, because according to the context I could need one transformer or another, or an IRI... Test all of them with "supportTransformation" and so, for every relation ? And what about circular ?
It's a little mess, my solution for now is to choose to return FooDto (without transform barDto) or to return an IRI, according to the context (which I am absolutly not sure of what I'm doing with it due to the lack of documentation about $context).
Same for BarTransformer.
So every transformer need to choose to actually transform the object without handeling transformation of relations, or return the correspondant IRI. That is the less dirty I found.
I guess you have two possible solutions here, to extend your DTO's constructor signature by one more argument and adjust your transformer or to do the transformation right inside your DTO's constructor:
<?php
namespace Dto;
class FooDto
{
public BarDto $bar;
// first variant
public function __construct(FooEntity $fooEntity, BarDto $barDto)
{
$this->id = $fooEntity->getId();
$this->bar = $barDto;
}
// second
public function __construct(FooEntity $fooEntity)
{
$this->id = $fooEntity->getId();
$this->bar = new BarDto($fooEntity->getBar());
}
}
I am trying to access an Eloquent attribute with Twig in Slim, and getting an error.
I have a Field and a Type object, and the relationship is as follows
class Field extends \Illuminate\Database\Eloquent\Model {
protected $table = 'fields';
public function type()
{
return $this->belongsTo('models\Type');
}
When doing {{ f }} (being f a field), the output is this:
{"field_id":"1","field_name":"Your name","form_id":"2","type_id":"1","placeholder":"Please give us your name"}
And when doing {{ f.type }}, the result is:
Message: An exception has been thrown during the rendering of a template ("Object of class Illuminate\Database\Eloquent\Relations\BelongsTo could not be converted to string") in "pages/editform.html" at line 97.
If I try to do {{ f.type.name }}, doesn't throw up an exception but doesn't print anything either.
If I do it in PHP
$fields = $form->fields;
var_dump($fields[0]->type->name);
The value gets output correctly.
Any ideas?,
Thanks
If you don't want to do an eager load, you can override the magic __isset method in your Field class to return true for the type relationship property:
public function __isset($name)
{
if (in_array($name, [
'type'
])) {
return true;
} else {
return parent::__isset($name);
}
}
Explanation
The problem is in the interaction between how Eloquent implements the "dynamic property" for a relation (in your case, f.type), and the rules that Twig uses for accessing "attributes" of a variable.
From the Twig documentation:
For convenience's sake foo.bar does the following things on the PHP layer:
check if foo is an array and bar a valid element;
if not, and if foo is an object, check that bar is a valid property;
if not, and if foo is an object, check that bar is a valid method (even if bar is the constructor - use __construct() instead);
if not, and if foo is an object, check that getBar is a valid method;
if not, and if foo is an object, check that isBar is a valid method;
if not, return a null value.
foo['bar'] on the other hand only works with PHP arrays:
check if foo is an array and bar a valid element;
if not, return a null value.
The key here is the part where it says "check that bar is a valid property". This means that on the level of PHP, Twig is calling isset on $f->type. Eloquent implements the magic __isset method in Model, so you might think that this wouldn't be a problem.
However, take a look at how it actually implements __isset for model relations:
/**
* Determine if an attribute exists on the model.
*
* #param string $key
* #return bool
*/
public function __isset($key)
{
return (isset($this->attributes[$key]) || isset($this->relations[$key])) ||
($this->hasGetMutator($key) && ! is_null($this->getAttributeValue($key)));
}
It determines whether a relation is "set" by looking in its array of loaded relationships (isset($this->relations[$key])). The problem is that if type hasn't been loaded yet, Eloquent will say that it is not "set".
Thus when Twig looks at $f->type, it will think that type is not a valid property, and move on to the next rule:
...if foo is an object, check that bar is a valid method
So now it will look for the method type(), which it finds. The only problem? type() (the method) returns a Relation (specifically, a BelongsTo), rather than a Type object. And BelongsTo objects aren't models.
If you want Twig to know that the property $f->type does indeed exist, you have two choices:
You can eager-load the related Type object along with your Field, as suggested by #roger-collins, or;
You can overload the __isset magic method in Field:
public function __isset($name)
{
if (in_array($name, [
'type'
])) {
return true;
} else {
return parent::__isset($name);
}
}
This will force Twig to recognize that type is a valid property for Field, even before the related model has actually been loaded.
I had the same issue and stumbled upon this question. After solving it myself, I thought I'd try to help you out.
Try doing a Eager Load on the model:
Field::with('type')->get()
This should allow you to do the following with no other issues.
{{ f.type }}
See more info here: http://laravel.com/docs/4.2/eloquent#eager-loading
In controller A, I load a model not associated with this controller. I'm interested in managing the model name of controller B with a single variable, so I don't have to manually change many lines if the table/model B's name changes.
For example below is the controller A's code:
public $modelBName = 'ModelB';
public function controller_a_function() {
$this->loadModel($this->modelBName); // I use the variable here for model B
$this->ModelB->model_b_function(); // COMMENT #1
}
Question:
For the line commented "COMMENT #1," how do I use the variable name instead of explicitly written out word 'ModelB'? This line appears multiple times throughout the code, and I would like to use the variable $modelBName if possible. ModelB will likely not change, but if it does for some reason, it would be nice to just change one variable instead of editting multiple lines.
The simple answer; use this:
$this->{$this->modelBName}->find('all');
Note the curly brackets {} around the property name. more information can be found in the manual;
http://php.net/manual/en/language.variables.variable.php
A cleaner approach may be a sort of 'factory' method;
/**
* Load and return a model
*
* #var string $modelName
*
* #return Model
* #throws MissingModelException if the model class cannot be found.
*/
protected function model($modelName)
{
if (!isset($this->{$modelName})) {
$this->loadModel($modelName);
}
return $this->{$modelName};
}
Which can be used like this;
$result = $this->model($this->modelBName)->find('all');
debug($result);
And, if you don't want to specify the model, but want it to return a '$this->modelBName' automatically;
/**
* Load and return the model as specified in the 'modelBName' property
*
* #return Model
* #throws MissingModelException if the model class cannot be found.
*/
protected function modelB()
{
if (!isset($this->{$this->modelBName})) {
$this->loadModel($this->modelBName);
}
return $this->{$this->modelBName};
}
Which can be used like this:
$result = $this->modelB()->find('all');
debug($result);
I think you are confused between model name and table name. You can set a model to use a different database table by using the $useTable property, for example:
class User extends AppModel {
public $useTable = 'users_table'; // Database table used
}
class Product extends AppModel {
public function foo() {
$this->loadModel('User');
$this->User->find('all');
}
}
You should never need to change the name of the model, and if the name of the database table changes you can simply update the $useTable property in the model.
I have an model with a relation, and I want to instantiate a new object of the relations type.
Example: A person has a company, and I have a person-object: now I
want to create a company-object.
The class of the companyobject is defined in the relation, so I don't think I should need to 'know' that class, but I should be able to ask the person-object to provide me with a new instance of type company? But I don't know how.
This is -I think- the same question as New model object through an association , but I'm using PHPActiveRecord, and not the ruby one.
Reason behind this: I have an abstract superclass person, and two children have their own relation with a type of company object. I need to be able to instantiate the correct class in the abstract person.
A workaround is to get it directly from the static $has_one array:
$class = $this::$has_one[1]['class_name'];
$company = new $class;
the hardcoded number can of course be eliminated by searching for the association-name in the array, but that's still quite ugly.
If there is anyone who knows how this is implemented in Ruby, and how the phpactiverecord implementation differs, I might get some Ideas from there?
Some testing has revealed that although the "search my classname in an array" looks kinda weird, it does not have any impact on performance, and in use it is functional enough.
You can also use build_association() in the relationship classes.
Simplest way to use it is through the Model's __call, i.e. if your relation is something like $person->company, then you could instantiate the company with $company = $person->build_company()
Note that this will NOT also make the "connection" between your objects ($person->company will not be set).
Alternatively, instead of build_company(), you can use create_company(), which will save a new record and link it to $person
In PHPActiveRecord, you have access to the relations array. The relation should have a name an you NEED TO KNOW THE NAME OF THE RELATIONSHIP/ASSOCIATION YOU WANT. It doesn't need to be the classname, but the classname of the Model you're relating to should be explicitly indicated in the relation. Just a basic example without error checking or gritty relationship db details like linking table or foreign key column name:
class Person extends ActiveRecord\Model {
static $belongs_to = array(
array('company',
'class_name' => 'SomeCompanyClass')
);
//general function get a classname from a relationship
public static function getClassNameFromRelationship($relationshipName)
foreach(self::$belongs_to as $relationship){
//the first element in all relationships is it's name
if($relationship[0] == $relationshipName){
$className = null;
if(isset($relationship['class_name'])){
$className = $relationship['class_name'];
}else{
// if no classname specified explicitly,
// assume the clasename is the relationship name
// with first letter capitalized
$className = ucfirst($relationship);
}
return $className
}
}
return null;
}
}
To with this function, if you have a person object and want an object defined by the 'company' relationship use:
$className = $person::getClassNameFromRelationship('company');
$company = new $className();
I'm currently using below solution. It's an actual solution, instead
of the $has_one[1] hack I mentioned in the question. If there is a
method in phpactiverecord I'm going to feel very silly exposing
msyelf. But please, prove me silly so I don't need to use this
solution :D
I am silly. Below functionality is implemented by the create_associationname call, as answered by #Bogdan_D
Two functions are added. You should probably add them in the \ActiveRecord\Model class. In my case there is a class between our classes and that model that contains extra functionality like this, so I put it there.
These are the 2 functions:
public function findClassByAssociation($associationName)
Called with the name of the association you are looking for.
Checks three static vars (has_many,belongs_to and has_one) for the association
calls findClassFromArray if an association is found.
from the person/company example: $person->findClassByAssociation('company');
private function findClassFromArray($associationName,$associationArray)
Just a worker-function that tries to match the name.
Source:
/**
* Find the classname of an explicitly defined
* association (has_one, has_many, belongs_to).
* Unsure if this works for standard associations
* without specific mention of the class_name, but I suppose it doesn't!
* #todo Check if works without an explicitly set 'class_name', if not: is this even possible (namespacing?)
* #todo Support for 'through' associations.
* #param String $associationName the association you want to find the class for
* #return mixed String|false if an association is found, return the class name (with namespace!), else return false
* #see findClassFromArray
*/
public function findClassByAssociation($associationName){
//$class = $this::$has_one[1]['class_name'];
$that = get_called_class();
if(isset($that::$has_many)){
$cl = $this->findClassFromArray($associationName,$that::$has_many);
if($cl){return $cl;}
}
if(isset($that::$belongs_to)){
$cl = $this->findClassFromArray($associationName,$that::$belongs_to);
if($cl){return $cl;}
}
if(isset($that::$has_one)){
$cl = $this->findClassFromArray($associationName,$that::$has_one);
if($cl){return $cl;}
}
return false;
}
/**
* Find a class in a php-activerecord "association-array". It probably should have a specifically defined class name!
* #todo check if works without explicitly set 'class_name', and if not find it like standard
* #param String $associationName
* #param Array[] $associationArray phpactiverecord array with associations (like has_many)
* #return mixed String|false if an association is found, return the class name, else return false
* #see findClassFromArray
*/
private function findClassFromArray($associationName,$associationArray){
if(is_array($associationArray)){
foreach($associationArray as $association){
if($association['0'] === $associationName){
return $association['class_name'];
}
}
}
return false;
}