I'm trying to use dynamic accessor for Laravel model's virtual attributes. Actually I want to handle the situation that if a property doesn't directly exist / doesn't exist in database, load it's value from config file.
I managed to handle it by writing a accessor for each single attribute, but I find it redundant and ugly. I'm sure it can be done more effectively.
class MyModel extends Model
{
public function getTitleAttribute()
{
return $this->loadAttributeFromConfig('title');
}
public function getSubtitleAttribute()
{
return $this->loadAttributeFromConfig('subtitle');
}
public function getTagAttribute()
{
return $this->loadAttributeFromConfig('tag');
}
public function getIconCssClassAttribute()
{
return $this->loadAttributeFromConfig('iconCssClass');
}
public function getBoxCssClassAttribute()
{
return $this->loadAttributeFromConfig('boxCssClass');
}
public function getBulletsAttribute()
{
return $this->loadAttributeFromConfig('bullets');
}
protected function loadAttributeFromConfig($attribute)
{
return config('myConfigAttributes.' . $this->name . '.' . $attribute);
}
}
$myModel->append(['title', 'subtitle', 'tag', 'iconCssClass', 'boxCssClass', 'bullets']);
My solution works but I consider it ugly.
This can actually be achieved rather easily using the __get magic method. You can override it on a base model class that you inherit or create a trait like so:
trait ConfigAttributes
{
/**
* #param string $key
*
* #return mixed
* #throws \Exception
*/
public function __get($key)
{
// Make sure the required configuration property is defined on the parent class
if (!property_exists($this, 'configAttributes')) {
throw new Exception('The $configAttributes property should be defined on your model class');
}
if (in_array($key, $this->configAttributes)) {
return $key;
}
return parent::__get($key);
}
/**
* #return array
*/
public function toArray()
{
// We need to override this method because we need to manually append the
// attributes when serializing, since we're not using Eloquent's accessors
$data = collect($this->configAttributes)->flip()->map(function ($v, $key) {
return $this->loadAttributeFromConfig($key);
});
return array_merge(parent::toArray(), $data->toArray());
}
/**
* #param string $attribute
*
* #return mixed
*/
protected function loadAttributeFromConfig($attribute)
{
return config('myConfigAttributes.' . $this->name . '.' . $attribute);
}
}
Then in you model class just import the trait and specify your custom fields:
class MyModel extends Model
{
use ConfigAttributes;
protected $configAttributes = [
'title',
'subtitle',
'tag',
'iconCssClass',
'boxCssClass',
'bullets'
];
}
Word of caution: be careful when overriding magic methods on classes defined by Laravel, because Laravel makes heavy use of them and if you're not careful you risk breaking other functionality.
Related
I cannot seem to apply filtering on all children defined in a tree model format with eager loading mechanism
Here is my model definition (works great):
class Section extends Model
{
[...]
/**
* #return HasOne
*/
public function parent()
{
return $this->hasOne(
self::class,
'Id',
'IdParent'
)->with('parent');
}
/**
* #return HasMany
*/
public function children()
{
return $this->hasMany(
self::class,
'IdParent',
'Id'
)->with('children');
}
[...]
}
Now I want to filter out recursive based on a 'criteria object'
public function getMachines(SectionCriteria $sectionCriteria = NULL)
{
/**
* #var $builder Builder|Section
*/
$builder = Section::with([
'children' => function ($query) use ($sectionCriteria) {
if ($sectionCriteria) {
foreach ($sectionCriteria->getFilterFlags() as $flagName => $flagValue) {
if ($flagValue) {
$query->whereFlag($flagName); //Custom implementation
} else {
$query->whereNotFlag($flagName); //Custom implementation
}
}
}
}
]);
This works bot it is applied to the first level of the tree.
My question would be: Is there a way to pass an object to the children() relation so I can apply filters recursive (which would apply to all levels)?
Something like, let's say:
P.S: This is not possible since only a callback is accepted as a parameter
public function children($parameters)
{
return $this->hasMany(
self::class,
'IdParent',
'Id'
)->with('children'=>$parameters);
}
What I wouldn't want to use (with respect to SOLID principles):
Make a static class variable which holds criteria
A global variable of any kind
I also tried to retrieve children recursive (and apply filters) but ended up with more queries so Eloquent is preety well optimized sooo...
I used technique #1 (Make a static class variable) though I do not really like it but it works.
Model
/**
* #var null|SectionCriteria
*/
public static $childrenFilter = NULL; //This can be whatever you need since it's static
/**
* #return HasMany
*/
public function children()
{
return $this->hasMany(
self::class,
'IdParent',
'Id'
)->with(['children'=>self::searchChild()]);
}
/**
* #return \Closure
*/
public function searchChild()
{
return function ($builder) {
if (Section::$childrenFilter) {
foreach ($sectionCriteria->getFilterFlags() as $flagName => $flagValue) {
if ($flagValue) {
$query->whereFlag($flagName); //Custom implementation
} else {
$query->whereNotFlag($flagName); //Custom implementation
}
}
}
};
}
/**
* #param SectionCriteria $criteria
*/
public static function setChildSearch(SectionCriteria $criteria)
{
Section::$childrenFilter = $criteria;
}
/**
* Remove the search criteria filter
*/
public static function clearChildSearch()
{
Section::$childrenFilter = NULL;
}
Repository (the actual usage)
/**
* #param SectionCriteria|NULL $sectionCriteria
* #return Section[]|Collection
*/
public function getMachines(SectionCriteria $sectionCriteria = NULL)
{
/**
* #var $builder Builder|Section
*/
$builder = Section::with(['children']); //Here I do not need the root elements to be filtered, If needed then use: Section::with(['children'=>Section::searchChild()])
Section::setChildSearch($sectionCriteria);
$builder->orderBy('Name');
$results = $builder->get();
Section::clearChildSearch();
return $results;
}
Again...not preety but it gets the job done
New: Another way (will test this out) would be to extend the Builder class
In our application, we use repositories for models that are fetched from the database. So, we have an abstract repository that knows about the database, has a loadById method to load a database record and an abstract getEntity method that creates an object for that specific repository. Example code:
abstract class EntityRepository {
/**
* #param int $id
* #return AbstractEntity
*/
public function loadById($id) {
$record = $this->db->loadById($id);
$entity = $this->getEntity();
return $this->inflate($record, $entity);
}
/**
* #return AbstractEntity
*/
protected abstract function getEntity();
}
class PeopleRepository extends EntityRepository {
protected function getEntity() {
return new PeopleEntity();
}
}
abstract class AbstractEntity {
private $id;
/**
* #return int
*/
public function getId() {
return $this->id;
}
/**
* #param int $id;
*/
public function setId($id) {
$this->id = $id;
}
}
class PeopleEntity extends AbstractEntity {
private $name;
/**
* #return string
*/
public function getName() {
return $this->name;
}
/**
* #param string $name;
*/
public function setName($name) {
$this->name= $name;
}
}
When using an instance of PeopleRepository and fetching a model through loadById, PhpStorm is not able to resolve the returned model to a concrete type, but provides only code completion for the functions of AbstractEntity. Is there any simple way to make it work?
In https://confluence.jetbrains.com/display/PhpStorm/PhpStorm+Advanced+Metadata, I've only found ways to make it work for concrete classes and their functions. So, enumerating all repository classes and all their ways of creating an entity might work. But I'd love to see an abstract way of defining like "All instances of EntityRepository will return an entity of that type defined in getEntity() when loading an entity"
I doubt there's a blanket way of doing this. Even using PHPStorm meta you have to be explicit for each case. Perhaps the way of doing this is by doing something like adding a repository facade e.g.
class RepositoryFacade {
public static function __callStatic($method, $args) {
if ($args[0] == People::class) {
array_shift($args);
return new PeopleRepository()->{$method}(...$args);
}
}
}
Then you might be able to typehint this using:
override(RepositoryFacade::loadById(0), type(0));
Of course the facade is not really the best pattern to be using in general so I can see how this might not be ideal.
my problem is getting the right type of object from a method, which is returning a "mixed" type due to inhreitance.
I've got a generic list class
class List
{
/**
* #var Item[]
*/
protected $items;
public function __construct()
{
$this->items = array();
}
/**
* #return Item[]
*/
public function getAll()
{
return $this->items;
}
/**
* #return Item
*/
public function getOne($index)
{
if (isset($this->items[$index])) {
return $this->items[$index];
}
return null;
}
}
containing element of type Item, which is a generic class either
class Item
{
/**
* #var string
*/
protected $name;
public function __construct($name)
{
$this->name = $name;
}
}
Such generic classes are extended by N different lists. Just an example
class UserList extends List
{
/* user-specific implementation */
}
class User extends Item
{
/* user-specific implementation */
}
In the client code
$user_list = new UserList();
foreach ($user_list->getAll() as $user) {
echo $user->getEmailAddr();
}
Inside the foreach I don't have code completion, because my getAll method (inherited from the father) is returning Item[], or mixed[], not a User[]. Same problem with getOne method.
I wouldn't like to have to override such methods.
Is there a more clever and elegant solution?
Thank you
I don't think there's any way for the IDE to infer the type automatically. Use a phpdoc type annotation:
foreach ($user_list->getAll() as $user) {
/** #var User $user */
echo $user->getEmailAddr();
}
See the related question PHPDoc type hinting for array of objects?
I've been doing a lot of reading on constructors and initialising variables, and I've came across a problem which I'm trying to solve. I'm trying to solve the lack of generics support by introducing a variable that needs to be initialised by the subclass.
<?php
abstract class Collection_Base {
protected $typeOf; // Must be initialised by the subclass
protected $collection = array();
}
class Cookie_Collection extends Collection_Base {
protected $typeOf = 'System\Web\Http_Cookie';
public function set ($item) {
if (!$item instanceof $this->typeOf) {
throw new \InvalidArgumentException;
}
$this->collection[] = $item;
}
}
?>
So I was wondering, is it bad practice to include variable which must be initialised by subclass constructor in PHP? Is there anything I need to be aware of when doing so?
While not directly related, I've used the following sources to gather my information:
http://docs.hhvm.com/manual/en/hack.otherrulesandfeatures.classinitialization.php
http://ralphschindler.com/2012/03/09/php-constructor-best-practices-and-the-prototype-pattern
SOLUTION
<?php
abstract class Collection_Base {
protected $collection = array();
public abstract function getType();
private function getTypeInternal () {
$type = $this->getType();
if (is_class($type)) {
throw new \UnexpectedValueException;
}
return $type;
}
public function set ($item) {
$type = $this->getTypeInternal();
if (!$item instanceof $type) {
throw new \InvalidArgumentException;
}
$this->collection[] = $item;
}
}
class Cookie_Collection extends Collection_Base {
protected $type = 'System\Web\Http_Cookie';
public function getType () {
return $this->type;
}
}
?>
I thought I recognized this as an anti-pattern, so I looked for where I read about it, but then I remembered it was this: http://en.wikipedia.org/wiki/Call_super, which isn't quite the same thing.
On to what you are doing however. There are a lot of similar libraries which use a practice like this, however they differ by enforcing the practice in way of abstract methods:
abstract class Collection_Base {
protected $typeOf;
protected $collection = array();
/**
* #return string
*/
public function getType()
{
if (null === $this->typeOf) {
$this->typeOf = $this->doGetType();
}
return $this->typeOf;
}
/**
* #return string
*/
abstract protected function doGetType();
}
class Cookie_Collection extends Collection_Base {
/**
* #param $item
*/
public function set ($item) {
if (!$item instanceof $this->getType()) {
throw new \InvalidArgumentException;
}
$this->collection[] = $item;
}
/**
* #inheritdoc
*/
protected function doGetType()
{
return 'System\Configuration\Configuration_Element';
}
}
Use a Factory pattern to hide constructor details ... see http://www.phptherightway.com/pages/Design-Patterns.html
Make Collection_Base an abstract class and define a method that returns appropriate class name:
abstract class Collection_Base
{
protected $collection = [];
public function add($item)
{
if (!$item instanceof $this->getType()) {
throw new \InvalidArgumentException();
}
$this->collection[] = $item;
}
abstract protected function getType();
}
class Collection_Cookie extends Collection_Base
{
protected function getType()
{
return Configuration_Element::class;
}
}
Using this approach it's not possible for other developers to forget about type "property".
EDIT:
Using a factory, as suggested by Luca Rocchi is also a very good idea.
well, i'm wondering how they use the application class as an array
for example in application file
vendor/laravel/framework/src/illuminate/Container/Application.php
in magic methods __set and __get they use $this as an array
here is the code
/**
* Dynamically access container services.
*
* #param string $key
* #return mixed
*/
public function __get($key)
{
return $this[$key];
}
/**
* Dynamically set container services.
*
* #param string $key
* #param mixed $value
* #return void
*/
public function __set($key, $value)
{
$this[$key] = $value;
}
but i don't understand how this works without thorwing any errors
i have tried something like that but it give me the following error
Here is My Code
class Container{
public function __get($key){
return $this[$key];
}
public function __set($key,$val){
$this[$key] = $val;
}
}
$app = new Container();
$app->test = 'ok';
echo $app->test;
Fatal error: Cannot use object of type Container as array in C:\xampp\htdocs\test\test.php on line 10
Any explaination for this please ?
by the way i'm using laravel v4.2.12
Laravel's Illuminate\Container\Container class implements PHP's ArrayAccess interface, which provides the array access syntax to a class. To implement this interface, you must provide an implementation for the offsetExists(), offsetGet(), offsetSet(), and offsetUnset() methods. Once your class correctly implements the ArrayAccess interface, you can use the array access syntax.
This is just one example of an ArrayAccess implementation, but you can try something like this:
// note the implementation
class Container implements ArrayAccess {
protected $data = array();
public function offsetExists($offset) {
return array_key_exists($offset, $this->data);
}
public function offsetGet($offset) {
return $this->data[$offset];
}
public function offsetSet($offset, $value) {
$this->data[$offset] = $value;
}
public function offsetUnset($offset) {
unset($this->data[$offset]);
}
public function __get($key) {
return $this[$key];
}
public function __set($key, $val) {
$this[$key] = $val;
}
}
$app = new Container();
$app->test = 'ok';
echo $app->test.PHP_EOL.PHP_EOL.print_r($app, true);
You can also look at the Illuminate\Container\Container implementation to see how Laravel actually does it.