Laravel: Tree filter children recursive - php

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

Related

Dynamic accesor / properties in Laravel 5.8

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.

Getting the child-type of an object from a method inherited from the father

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?

Does it hurt Demeter's law when model refers to another model?

I have a global container class:
final class Container
{
/**
* #return ForumThread
*/
public static function getForumThread()
{
if (self::$obj1 === null)
{
self::$obj1 = new ForumThread();
}
return self::$obj1;
}
/**
* #return ForumPosts
*/
public static function getForumPosts()
{
if (self::$obj2 === null)
{
self::$obj2 = new ForumPosts();
}
return self::$obj2;
}
}
my models:
class ForumThread
{
/**
* #return bool
*/
public function findBadLanguage ($inWhat)
{
return (bool)rand(0,1);
}
/**
* #return
*/
public function add ($threadName)
{
if (!$this->findBadLanguage ($threadName))
{
INSERT INTO
}
}
}
class ForumPost
{
/**
* #return
*/
public function post ($toThreadId, $comment)
{
// im talking about this:
Container::getForumThread()->findBadLanguage($comment);
}
}
I know findBadLanguage() should be in another class, but lets suppose thats okay. Lets focus on Container::get****() calls. Is it OK to turn to a global container and get objects from it? Doesnt it hury Demeter's law? (those object must be exists only once, and can be DI-ed)
EDIT: you can regard to the Container as a Factory

Paris ORM, has_many_through with restrictions

What's the best way to approach this in Paris ORM?
I have a set of categories and a set of supplier profiles that havw a column called reatured. Currently my class is as follows:
<?php
namespace {
/**
* Class Category
*/
class Category extends ConfettiModel
{
public static $_table = 'supplier_directory_category';
/**
* Returns only top level categories - they have no parent
*
* #return bool
*/
public static function topLevel()
{
return self::where('parent', 0);
}
public static function marketing()
{
return self::where('marketing', 'Yes');
}
public function getTable() {
return self::$_table;
}
/**
* Is this a top level category - has no parent
*
* #return bool
*/
public function isTopLevel()
{
return ($this->parentId == 0);
}
/**
* Associated DirectoryProfile's
*
* #return ORMWrapper
*/
public function profiles()
{
return $this->has_many_through('DirectoryProfile', 'CategoryDirectoryProfile', 'category', 'supplier');
}
}
I'd like to add a new function, featuredProfiles() that allows me to retrieve the same results as profiles(), but in this case I want to restrict it to suppliers with featured = 'Yes'.
I'm not quite sure how to make this happen.
I took a punt and the answer was easier than I anticipated:
public function featuredProfiles() {
return $this->profiles()->where('featured', 'Yes');
}
The where is added as part of the query on the joined table.

method does not exist on this mock object - Laravel , Mockery

i'm trying to test a simple class. I'm following this tutorial( http://code.tutsplus.com/tutorials/testing-laravel-controllers--net-31456 ).
I have this error, while running tests:
Method Mockery_0_App_Interfaces_MealTypeRepositoryInterface::getValidator() does not exist on this mock object
Im using repository structure. So, my controller calls repository and that returns Eloquent's response.
I'm relatively new in php and laravel. And I've started learning to test a few days ago, so I'm sorry for that messy code.
My test case:
class MealTypeControllerTest extends TestCase
{
public function setUp()
{
parent::setUp();
$this->mock = Mockery::mock('App\Interfaces\MealTypeRepositoryInterface');
$this->app->instance('App\Interfaces\MealTypeRepositoryInterface' , $this->mock);
}
public function tearDown()
{
Mockery::close();
}
public function testIndex()
{
$this->mock
->shouldReceive('all')
->once()
->andReturn(['mealTypes' => (object)['id' => 1 , 'name' => 'jidlo']]);
$this->call('GET' , 'mealType');
$this->assertViewHas('mealTypes');
}
public function testStoreFails()
{
$input = ['name' => 'x'];
$this->mock
->shouldReceive('getValidator')
->once()
->andReturn(Mockery::mock(['fails' => true]));
$this->mock
->shouldReceive('create')
->once()
->with($input);
$this->call('POST' , 'mealType' , $input ); // this line throws the error
$this->assertRedirectedToRoute('mealType.create');//->withErrors();
$this->assertSessionHasErrors('name');
}
}
My EloquentMealTypeRepository:
Nothing really interesting.
class EloquentMealTypeRepository implements MealTypeRepositoryInterface
{
public function all()
{
return MealType::all();
}
public function find($id)
{
return MealType::find($id);
}
public function create($input)
{
return MealType::create($input);
}
public function getValidator($input)
{
return MealType::getValidator($input);
}
}
My eloquent implementation:
Nothing really interresting,too.
class MealType extends Model
{
private $validator;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'meal_types';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['name'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = [];
public function meals()
{
return $this->hasMany('Meal');
}
public static function getValidator($fields)
{
return Validator::make($fields, ['name' => 'required|min:3'] );
}
}
My MealTypeRepositoryInterface:
interface MealTypeRepositoryInterface
{
public function all();
public function find($id);
public function create($input);
public function getValidator($input);
}
And finally, My controller:
class MealTypeController extends Controller {
protected $mealType;
public function __construct(MealType $mealType)
{
$this->mealType = $mealType;
}
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index()
{
$mealTypes = $this->mealType->all();
return View::make('mealTypes.index')->with('mealTypes' ,$mealTypes);
}
/**
* Show the form for creating a new resource.
*
* #return Response
*/
public function create()
{
$mealType = new MealTypeEloquent;
$action = 'MealTypeController#store';
$method = 'POST';
return View::make('mealTypes.create_edit', compact('mealType' , 'action' , 'method') );
}
/**
* Validator does not work properly in tests.
* Store a newly created resource in storage.
*
* #return Response
*/
public function store(Request $request)
{
$input = ['name' => $request->input('name')];
$mealType = new $this->mealType;
$v = $mealType->getValidator($input);
if( $v->passes() )
{
$this->mealType->create($input);
return Redirect::to('mealType');
}
else
{
$this->errors = $v;
return Redirect::to('mealType/create')->withErrors($v);
}
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id)
{
return View::make('mealTypes.show' , ['mealType' => $this->mealType->find($id)]);
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return Response
*/
public function edit($id)
{
$mealType = $this->mealType->find($id);
$action = 'MealTypeController#update';
$method = 'PATCH';
return View::make('mealTypes.create_edit')->with(compact('mealType' , 'action' , 'method'));
}
/**
* Update the specified resource in storage.
*
* #param int $id
* #return Response
*/
public function update($id)
{
$mealType = $this->mealType->find($id);
$mealType->name = \Input::get('name');
$mealType->save();
return redirect('mealType');
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return Response
*/
public function destroy($id)
{
$this->mealType->find($id)->delete();
return redirect('mealType');
}
}
That should be everything. It's worth to say that the application works, just tests are screwed up.
Does anybody know, why is that happening? I cant see a difference between methods of TestCase - testIndex and testStoreFails, why method "all" is found and "getValidator" is not.
I will be thankful for any tips of advices.
Perhaps an aside, but directly relevant to anyone finding this question by its title:
If:
You are getting the error BadMethodCallException: Method Mockery_0_MyClass::myMethod() does not exist on this mock object, and
none of your mocks are picking up any of your subject's methods, and
your classes are being autoloaded, (e.g. using composer)
then before making your mock object, you need to force the loading of that subject, by using this line of code:
spl_autoload_call('MyNamespace\MyClass');
Then you can mock it:
$mock = \Mockery::mock('MyNamespace\MyClass');
In my PHPUnit tests, I often put that first line into the setUpBeforeClass() static function, so it only gets called once and is isolated from tests being added/deleted. So the Test class looks like this:
class MyClassTest extends PHPUnit_Framework_TestCase {
public static function setUpBeforeClass() {
parent::setUpBeforeClass();
spl_autoload_call('Jodes\MyClass');
}
public function testIt(){
$mock = \Mockery::mock('Jodes\MyClass');
}
}
I have forgotten about this three times now, each time spending an hour or two wondering what on earth the problem was!
I have found a source of this bug in controller.
calling wrong
$v = $mealType->getValidator($input);
instead of right
$v = $this->mealType->getValidator($input);

Categories