Get all relationships from Eloquent model - php

Having one Eloquent model, is it possible to get all its relationships and their type at runtime?
I've tried taking a look at ReflectionClass, but I couldn't find anything useful for this scenario.
For example, if we have the classic Post model, is there a way to extract relationships like this?
- belongsTo: User
- belongsToMany: Tag

To accomplish this, you will have you know the names of the methods within the model - and they can vary a lot ;)
Thoughts:
if you got a pattern in the method, like relUser / relTag, you can filter them out
or loop over all public methods, see if a Relation object pops up (bad idea)
you can define a protected $relationMethods (note: Laravel already uses $relations) which holds an array with method.
After calling Post->User() you will receive a BelongsTo or 1 of the other objects from the Relation family, so you can do you listing for the type of relation.
[edit: after comments]
If the models are equipped with a protected $with = array(...); then you are able to look into the loaded relations with $Model->getRelations() after a record is loaded. This is not possible when no record is loaded, since the relations aren't touched yet.
getRelations() is in /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
But currently it doesn't show up in the api at laravel.com/api - this is because we got newer version

Like Rob stated. It is a bad idea to loop through every public method and check out if a relation is returned.
Barryvdh uses a Regex based approach in his very popular Laravel-ide-helper:
https://github.com/barryvdh/laravel-ide-helper/blob/master/src/Console/ModelsCommand.php
You just have to filter the properties you receive after calling getPropertiesFromMethods like this (untested example):
class classSniffer{
private $properties = [];
//...
public function getPropertiesFromMethods($model){
//the copied code from the class above (ModelsCommand#getPropertiesFromMethods)
}
public function getRelationsFrom($model){
$this->getPropertiesFromMethods($model);
$relations = [];
foreach($this->properties as $name => $property){
$type = $property;
$isRelation = strstr($property[$type], 'Illuminate\Database\Eloquent\Relations');
if($isRelation){
$relations[$name] = $property;
}
}
return $relations;
}
}
Is there a cleaner way of doing that without touching the Models?
I think we have to wait for PHP7 (Return Type Reflections) or for a new Reflection Service from Taylor ^^

I've been working on the same thing lately, and I don't think it can effectively be done without Reflection. But this is a little resource-intensive, so I've applied some caching. One check that's needed is to verify the return type, and pre-php7, that can only be done by actually executing each method. So I've also applied some logic that reduces the number of likely candidates before running that check.
/**
* Identify all relationships for a given model
*
* #param object $model Model
* #param string $heritage A flag that indicates whether parent and/or child relationships should be included
* #return array
*/
public function getAllRelations(\Illuminate\Database\Eloquent\Model $model = null, $heritage = 'all')
{
$model = $model ?: $this;
$modelName = get_class($model);
$types = ['children' => 'Has', 'parents' => 'Belongs', 'all' => ''];
$heritage = in_array($heritage, array_keys($types)) ? $heritage : 'all';
if (\Illuminate\Support\Facades\Cache::has($modelName."_{$heritage}_relations")) {
return \Illuminate\Support\Facades\Cache::get($modelName."_{$heritage}_relations");
}
$reflectionClass = new \ReflectionClass($model);
$traits = $reflectionClass->getTraits(); // Use this to omit trait methods
$traitMethodNames = [];
foreach ($traits as $name => $trait) {
$traitMethods = $trait->getMethods();
foreach ($traitMethods as $traitMethod) {
$traitMethodNames[] = $traitMethod->getName();
}
}
// Checking the return value actually requires executing the method. So use this to avoid infinite recursion.
$currentMethod = collect(explode('::', __METHOD__))->last();
$filter = $types[$heritage];
$methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); // The method must be public
$methods = collect($methods)->filter(function ($method) use ($modelName, $traitMethodNames, $currentMethod) {
$methodName = $method->getName();
if (!in_array($methodName, $traitMethodNames) //The method must not originate in a trait
&& strpos($methodName, '__') !== 0 //It must not be a magic method
&& $method->class === $modelName //It must be in the self scope and not inherited
&& !$method->isStatic() //It must be in the this scope and not static
&& $methodName != $currentMethod //It must not be an override of this one
) {
$parameters = (new \ReflectionMethod($modelName, $methodName))->getParameters();
return collect($parameters)->filter(function ($parameter) {
return !$parameter->isOptional(); // The method must have no required parameters
})->isEmpty(); // If required parameters exist, this will be false and omit this method
}
return false;
})->mapWithKeys(function ($method) use ($model, $filter) {
$methodName = $method->getName();
$relation = $model->$methodName(); //Must return a Relation child. This is why we only want to do this once
if (is_subclass_of($relation, \Illuminate\Database\Eloquent\Relations\Relation::class)) {
$type = (new \ReflectionClass($relation))->getShortName(); //If relation is of the desired heritage
if (!$filter || strpos($type, $filter) === 0) {
return [$methodName => get_class($relation->getRelated())]; // ['relationName'=>'relatedModelClass']
}
}
return false; // Remove elements reflecting methods that do not have the desired return type
})->toArray();
\Illuminate\Support\Facades\Cache::forever($modelName."_{$heritage}_relations", $methods);
return $methods;
}

I have the same needs on my project. My solution is using get_class function to check type of relation. example:
$invoice = App\Models\Invoice::with('customer', 'products', 'invoiceProducts', 'invoiceProduct')->latest()->first();
foreach ($invoice->getRelations() as $relation => $items) {
$model = get_class($invoice->{$relation}());
$type = explode('\\', $model);
$type = $type[count($type) - 1];
$relations[] = ['name' => $relation, 'type' => $type];
}
dd($relations);
example result:
array:4 [▼
0 => array:2 [▼
"name" => "customer"
"type" => "BelongsTo"
]
1 => array:2 [▼
"name" => "products"
"type" => "BelongsToMany"
]
2 => array:2 [▼
"name" => "invoiceProducts"
"type" => "HasMany"
]
3 => array:2 [▼
"name" => "invoiceProduct"
"type" => "HasOne"
]
]
I need it for duplicate an model item including the relation

composer require adideas/laravel-get-relationship-eloquent-model
https://packagist.org/packages/adideas/laravel-get-relationship-eloquent-model
Laravel get relationship all eloquent models!
You don't need to know the names of the methods in the model to do this. Having one or many Eloquent models, thanks to this package, you can get all of its relationships and their type at runtime

I know its bit late, but I have been visiting this question multiple times so thought to share my observations to help those who visits this question in future.
Here is the method i used to extract the relationships from an eloquent model class.
/**
*
* Returns all the relationship methods defined
* in the provided model class with related
* model class and relation function name
*
* #param string $modelClass exampe: App\Models\Post
* #return array $relattions array containing information about relationships
*/
protected function getModelRelationshipMethods(string $modelClass)
{
//can define this at class level
$relationshipMethods = [
'hasMany',
'hasOne',
'belongsTo',
'belongsToMany',
];
$reflector = new ReflectionClass($modelClass);
$path = $reflector->getFileName();
//lines of the file
$lines = file($path);
$methods = $reflector->getMethods();
$relations = [];
foreach ($methods as $method) {
//if its a concrete class method
if ($method->class == $modelClass) {
$start = $method->getStartLine();
$end = $method->getEndLine();
//loop through lines of the method
for($i = $start-1; $i<=$end-1; $i++) {
// look for text between -> and ( assuming that its on one line
preg_match('~\->(.*?)\(~', $lines[$i], $matches);
// if there is a match
if (count($matches)) {
//loop to check if the found text is in relationshipMethods list
foreach ($matches as $match) {
// if so add it to the output array
if (in_array($match, $relationshipMethods)) {
$relations[] = [
//function name of the relation definition
'method_name' => $method->name,
//type of relation
'relation' => $match,
//related Class name
'related' => (preg_match('/'.$match.'\((.*?),/', $lines[$i], $related) == 1) ? $related[1] : null,
];
}
}
}
}
}
}
return $relations;
}
If you dd() or dump() the returned $relations for the App/Post model, The output will be something like this
^ array:3 [
0 => array:3 [
"method_name" => "user"
"relation" => "belongsTo"
"related" => "User::class"
]
1 => array:3 [
"method_name" => "tag"
"relation" => "belongsToMany"
"related" => "Tag::class"
]
2 => array:3 [
"method_name" => "comments"
"relation" => "hasMany"
"related" => "Comment::class"
]
]

Related

Laravel Cache helper "driver" not implemented

I have a test that is calling a Controller, which literally handles a call to some logic service I mocked.
The test
public function test_it_can_list_all_folders()
{
$mockedLogicResponse = [
"id" => 1111,
"parent_id" => 2222,
"name" => "Some Name",
"children" => [
[
"id" => 3333,
"parent_id" => 5555,
"name" => "Ad-hoc"
],
[
"id" => 4444,
"parent_id" => 6666,
"name" => "Another thing"
]
]
];
Cache::shouldReceive('has')
->once()
->with('campaign_folders')
->andReturn(false);
$this->instance(
Logic::class,
Mockery::mock(Logic::class, function (MockInterface $mock) use ($mockedLogicResponse) {
$mock->shouldReceive('fetchData')
->once()
->andReturn($mockedLogicResponse);
})
);
// httpGet is just a wrapper for a call('GET', ..), it's tested and working fine
$response = $this->httpGet($route);
$response->assertOk(); //This goes well
$this->assertEquals($mockedLogicResponse, $response->json()); //This goes well too
}
Controller:
class LogicController extends Controller {
protected $logic;
public function __construct(Logic $logic)
{
$this->logic = $logic;
}
public function index(Request $request)
{
$id = $request->get('folder_id');
return $this->logic->fetchData($id);
}
}
Logic:
class Logic {
public function fetchData(string $id): array
{
if (Cache::has('folders')) {
return Cache::get('folders');
}
//This is returning correctly the data
$foldersList = $this->getFolders(...);
foreach ($foldersList[$folder['id']] as $folder) {
$res = [....];
// We perform some irrelevant logic
$children = ['children' => $res[$folder['id']]];
$fetchedFolders[] = array_merge($folder, $children);
}
Cache::put('folders', $fetchedFolders, 3600);
return $fetchedFolders;
}
}
The problem(s) are a few, for starters I'm receiving this:
Mockery\Exception\BadMethodCallException : Received Mockery_2_Illuminate_Cache_CacheManager::driver(), but no expectations were specified
It's good to point out that I am literally copying an example from the documentation here Laravel docs, so I can't see I'm missing any step.
Also, as the Cache is being called from the mocked logic, but the method is calling them (I dumped the result of the "has" Cache method)
As I also want to test (In another test) that the Cache::get() is begin called when requested the data for a second time, how can I clean the Cache (I set it for an hour), in order to test something like so:
Cache::shouldReceive('has')->twice();
Cache::shouldReceive('get')->once();
Cache::shouldReceive('put')->once();
Is there any step I am missing? If so, which ones?
UPDATE: After googling a bit, I found this solution, which in part solves my testing issues, but I'm concerned why the official documentation is not working, in order to use it instead of a custom solution.
Kind regards

Laravel manage different API responses

Explanation
User scans barcode and system responses with a barcodable model (can be Article, Package or Inventory Shelf).
return new BarcodeResource($barcode);
Barcode resource resolves barcodable resource based on barcodable class. Each barcodable model return different JSON resouce.
// BarcodeResource.php
$modelResource = app()->makeWith(__NAMESPACE__ . '\\' . class_basename($this->barcodable) . 'Resource', [
'resource' => $this->barcodable
]);
return [
'code' => $this->code,
'model_type' => class_basename($this->barcodable),
'model_data' => $modelResource
];
In case of...
... Article, I'd like to print packages that contain those kind of articles
... Package, I'd like to print location (inventory shelf), included articles and child packages
... Inventory Shelf, I'd like to print all packages
Problem
I want to prevent infinity loops with recursive resources.
Article
>> Package
>> Article (infinity loop begins because package resource
returns articles in spesific package)
Package
>> Article
>> Package (loop...)
>> Inventory Shelf
>> Package (loop...)
>> Child package
Inventory Shelf
>> Package
>> Article
>> Inventory Shelf (loop...)
>> Child package
Eager loading and unsetting relations should be one solution, but how I can unset those in the correct phase? Is this even possible with one resources or should I make multiple resources (recursive/normal)?
Tries
Extra attribute containing relations
I tried this solution, but magically $this->relations attribute gets changed to integer 1 after couple recursions...
class PackageResource extends JsonResource
{
private $relations;
public function __construct($resource, array $relations = [])
{
parent::__construct($resource);
$this->relations = $relations;
}
public function toArray($request)
{
return [
'id' => $this->id,
'articles' => $this->when(in_array('articles', $this->relations), ArticleResource::collection($this->articles, $this->relations)),
'children' => PackageResource::collection($this->children, $this->relations),
];
}
My solution for a similar situation was as follows:
In the Resource files, I allways return relationships based on a request property with. This is attached to the request as follows:
I need the User with Orders and Profile, but I also need the Area for an order, than the request is something like this:
http://example.com/api/v1/user/234?with=user.orders,user.profile,orders.area
and in the Resource file something similar:
public function toArray($request)
{
$return = [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'location' => $this->location,
'active' => $this->isActive(),
'level' => $this->level,
];
if($request->has('with')){
$relationships = [
'orders'=>[OrderCollection::class, 'orders'],
'area'=>[Area::class, 'area', 'area.admin'],
'profile'=>[UserProfile::class, 'profile'],
];
$with = explode(',', $request->with);
foreach($relationships as $key => $relationship){
if( in_array("user.".$key, $with) ){
$return[$key] = new $relationship[0]($this->{$relationship[1]});
}
}
}
$return['created'] = $this->created_at->toDateTimeString();
return $return;
}
An other solution is to add an extra property to the resource class:
protected $with = "";
public function __construct(mixed $resource, $with="")
{
parent::__construct($resource);
}
Than, when you call that resource, you can filter it in the previous way. I just tested and it worked for me.
Hope that helps.

In CakePhp, how can I retrieve the value of only one column from my database?

I am new to CakePHP but I have been using PHP for a while. I am trying to create a helper that would provide the level of access of a user (ACL).
Here is my ACLHelper.php so far
<?php
namespace App\View\Helper;
use Cake\View\Helper;
use Cake\ORM\TableRegistry;
class ACLHelper extends Helper{
public function getACL($id, $acl_field, $level){
$members = TableRegistry::get('groups_member');
$group = $members->find()->where(['user_id' => $id]);
$acls = TableRegistry::get('acls');
$acl = $acls->find('all', [ 'fields' => $acl_field ])->where(['group_id' => $group->first()->group_id]);
return $acl->first();
}
}
I call this function in my view this way
<?= $this->ACL->getACL($user->id, 'is_items', '4') ?>
And this is the output
{ "is_items": "4" }
What I need is the function to return true or false if the value of the field equals or is higher then the value of $level provided to the function. Now if I do this :
<?= $this->ACL->getACL($user->id, 'is_items', '4')->is_item ?>
it will return just the value. My problem is that I do not want to specify the field twice.
Thanks in advance for any help
public function getACL($id, $acl_field, $level){
$members = TableRegistry::get('groups_member');
$group = $members->find()->where(['user_id' => $id]);
$acls = TableRegistry::get('acls');
// Get the first ACL record right here
$acl = $acls->find('all', [ 'fields' => $acl_field ])->where(['group_id' => $group->first()->group_id])->first();
// Compare the requested field against the provided level
return $acl->$acl_field >= $level;
}

Laravel Unit Testing, how to "seeInDatabase" soft deleted row?

I'm working on a small unit test where I soft delete a row. To mark the test as successful I have to find that row with:
a given ID and
deleted_at column should not be null.
I can fulfil first condition - because obviously I know the ID.
Unfortunately I don't know how to tell seeInDatabase method that I expect deleted_at not to be null:
$this->seeInDatabase(
'diary_note_categories',
[
'id' => 'a7e35ad0-6f00-4f88-b953-f498797042fc',
'deleted_at' => null // should be is not null, like <> or != or whatever
]
);
Any hints?
'deleted_at <>' => null breaks
'deleted_at' => ['!=' => null] breaks as well
I did it in this way:
$this->seeInDatabase('diary_note...',['id' => 'a7e35ad0'])
->notSeeInDatabase('diary_note...',['id' => 'a7e35ad0','deleted_at'=>null]);
So I'm checking in two steps
I check if there is a record with our id in the table
I check if there is no record with our id and deleted_at = null in the table
It's not currently possible. Both seeInDatabase and notSeeInDatabase just pass the array directly to the where method of the query builder and that doesn't understand how to deal with anything other than = when passed an array.
https://github.com/laravel/framework/blob/2b4b3e3084d3c467f8dfaf7ce5a6dc466068b47d/src/Illuminate/Database/Query/Builder.php#L452
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
// If the column is an array, we will assume it is an array of key-value pairs
// and can add them each as a where clause. We will maintain the boolean we
// received when the method was called and pass it into the nested where.
if (is_array($column)) {
return $this->whereNested(function ($query) use ($column) {
foreach ($column as $key => $value) {
$query->where($key, '=', $value);
}
}, $boolean);
}
// ...
}
Option 1 - Add the following code to your TestCase class which you extend your test cases from
Gist: https://gist.github.com/EspadaV8/73c9b311eee96b8e8a03
<?php
/**
* Assert that a given where condition does not matches a soft deleted record
*
* #param string $table
* #param array $data
* #param string $connection
* #return $this
*/
protected function seeIsNotSoftDeletedInDatabase($table, array $data, $connection = null)
{
$database = $this->app->make('db');
$connection = $connection ?: $database->getDefaultConnection();
$count = $database->connection($connection)
->table($table)
->where($data)
->whereNull('deleted_at')
->count();
$this->assertGreaterThan(0, $count, sprintf(
'Found unexpected records in database table [%s] that matched attributes [%s].', $table, json_encode($data)
));
return $this;
}
/**
* Assert that a given where condition matches a soft deleted record
*
* #param string $table
* #param array $data
* #param string $connection
* #return $this
*/
protected function seeIsSoftDeletedInDatabase($table, array $data, $connection = null)
{
$database = $this->app->make('db');
$connection = $connection ?: $database->getDefaultConnection();
$count = $database->connection($connection)
->table($table)
->where($data)
->whereNotNull('deleted_at')
->count();
$this->assertGreaterThan(0, $count, sprintf(
'Found unexpected records in database table [%s] that matched attributes [%s].', $table, json_encode($data)
));
return $this;
}
Option 2 - Install the following composer package
This composer package is the exact same code as above, but packaged up for Composer.
composer require kirkbater/soft-deletes
Then use it inside of your specific test class:
<?php
use Kirkbater\Testing\SoftDeletes;
class MyTestClass extends TestClass {
use SoftDeletes;
}
This is an old question, but for those using more recent versions of Laravel (5.4 and above), there is now an assertSoftDeleted assertion: documentation.
So the answer to the original question would now be:
$this->assertSoftDeleted('diary_note_categories', [
'id' => 'a7e35ad0-6f00-4f88-b953-f498797042fc'
]);
Assert the given record has been deleted (Laravel 5.4 and above).
assertSoftDeleted(string|Model $table, array $data = [], string|null $connection = null)
Example with id:
$this->assertSoftDeleted('table_name', ['id'='value'])
Example with model:
$user = User::factory()->create();
$user->delete();
$this->assertSoftDeleted($user);
I used in Laravel 6
$this->assertDatabaseMissing('stores', [
'id' => $test_data['store']->id, 'deleted_at' => null
]);
$this->assertDatabaseHas('stores', ['id' => $id]);
It is not tested, but try like this :
$this->seeInDatabase(
'diary_note_categories',
[
'id' => 'a7e35ad0-6f00-4f88-b953-f498797042fc',
'deleted_at' => ['deleted_at' ,'!=', null ] // should be is not null, like <> or != or whatever
]
);

'where' condition in has_many_through in kohana

I have two models Illness and Symptom:
Illness
class Model_Illness extends ORM {
//protected $_db_group = 'default2';
protected $_table_name = 'illnesses';
protected $_has_many = array(
'symptoms' => array(
'through' => 'symptoms_illnesses',
'foreign_key' => 'illness_id',
)
);
Symptom
class Model_Symptom extends ORM {
//protected $_db_group = 'default2';
protected $_table_name = 'symptoms';
protected $_has_many = array(
'illnesses' => array(
'through' => 'symptoms_illnesses',
'foreign_key' => 'symptom_id',
)
);
The logic is illness may have many symptoms and symptom may have many illnesses. So these two have middle table symptoms_illnesses which stores ids of interconnected illnesses and symptoms.
Finally my task: I have to make search of illnesses by symptoms. I send symptom ids in array, and then should get illnesses which only have these symptoms. I tried following function:
public function bySymp($symps){
$res = array();
$objs = ORM::factory('Illness')->join('symptoms_illnesses')
->on('symptoms_illnesses.illness_id', '=', 'illness.id');
foreach($symps as $s){
$objs = $objs->where('symptoms_illnesses.symptom_id', '=', $s);
}
foreach($objs->find_all() as $o){
$res[] = $o;
}
return $res;
}
It returns nothing, when I put more than one value in array. I also tried $objs = $this->where('symptom_id', 'IN', $symptom_ids); it works like 'OR' condition, but I have to output exactly such illnesses that have symtoms in array of symptoms id's.
I think you would need to do multiple joins, one for each sympton. But a quick look into the Kohana documentation shows, that it doesn't allow for aliases inside queries, so a constructing a WHERE clause is difficult/impossible.
The only way I see that this works out of the box would be first finding all illnesses and then checking via has() for the required symptons.
$res = array();
$objs = ORM::factory('Illness')->find_all();
foreach ($objs as $o) {
if ($o->has('symptom', $symps)) {
$res[] = $o;
}
}
return $res;
But I haven't worked with Kohana in some time and might be overlooking something.

Categories