I have a system that might have a lot of controllers in the future. What I wanted to achieve (and achieved) is to make some form of dynamic routes, in order not to flood routes/web.php file with tons of route groups (there might be more than 300 routes).
So I did this:
routes/web.php:
Route::get('/report/{report_name}', [ReportController::class, 'index'])->name('report');
Route::post('/report/{report}/{method_name}', [ReportController::class, 'getRpcCall'])->name('report'); // This one used for RPC call (like Vue.js/AJAX calls)
ReportController.php:
public function index(string $reportName = null)
{
if (!class_exists(self::REPORTS_NAMESPACE . $reportName . 'Controller')) {
throw new ReportClassNotFoundException($reportName);
}
$report = app(self::REPORTS_NAMESPACE . $reportName . 'Controller');
return $report->index();
}
public function getRpcCall(Request $request, string $report = null, string $methodName = null): mixed
{
if (!class_exists(self::REPORTS_RPC_NAMESPACE . $report . 'RpcController')) {
throw new ReportClassNotFoundException($report);
}
$report = app(self::REPORTS_RPC_NAMESPACE . $report . 'RpcController');
$method = $methodName;
if (!method_exists($report, $method)) {
throw new MethodNotFoundException($report, $methodName);
}
return $report->$method($request);
}
But since I am a big fan of using FormRequests (in order not to bloat Controller with stuff that shouldn't be there, according to the SRP), I created a controller for reporting, that is being called dynamically, using above methods, like this: {{ route('rpc.report', ['FuelReporting', 'updateFuelCardData']) }}. Here it is:
class SomeReportingRpcController extends Controller
{
public function updateFuelCardData(SomeModelUpdateRequest $request): Response
{
// Some stuff going on here
}
}
As you can see, I use SomeModelUpdateRequest as a form request (created via php artisan make:request SomeModelUpdateRequest) for validation purposes. But the problem is that Laravel throws an error that
Argument #1 ($request) must be of type App\Http\Requests\SomeModelUpdateRequest, Illuminate\Http\Request given, called in ReportController.php
As I thought, that even if SomeModelUpdateRequest extends FormRequest (which extends Illuminate\Http\Request class), it should be fine, since method param type hinting is not conflicting with parent and child classes, but I was wrong.
Any ideas on how to keep the "dynamic route" logic and be able to use Form Requests?
I already have a working solution to ditch the Form request and use $request->validate() in some service class, but it is not so clean and I won't be able to use $request->validated() after that, that is kind of crucial for me.
Kind regards,
I just can't understand how to do in "the laravel way" what the example below describes without messing with model properties from the database.
To be clear, I'm looking for some core Laravel magic so I don't need to do the $memoryCache thing manually
Imagine the getProp() method is a database call that I don't want to run more than once, ie keeping the result in memory until PHP is done running.
Example class:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Test extends Model
{
public $memoryCache = [];
public function propA() {
return $this->getProp() . '_A';
}
public function propB() {
return $this->getProp() . '_B';
}
private function getProp() {
if (!array_key_exists('prop', $this->memoryCache)) {
echo "Do this only once\n";
$this->memoryCache['prop'] = 'DBData_' . rand(1,5);
}
return $this->memoryCache['prop'];
}
}
Running it:
$test = new \App\Models\Test;
echo "{$test->propA()}\n";
echo "{$test->propB()}\n";
Output:
Do this only once
DBData_5_A
DBData_5_B
Bonus!
Would be even better if I could somehow use PHP getters/setters so that I could just call them like class "properties" like this:
$test = new \App\Models\Test;
echo "{$test->propA}\n";
echo "{$test->propB}\n";
There really isn't anything built in anywhere that you could use, what you are doing is fine; this is the same type of check that Eloquent does when you use the dynamic property for a relationship. It checks if it has been loaded and if not loads it then returns the loaded relationship (that is saved on the instance, cached in the same type of way).
The "Bonus" part:
If you want to access those as properties instead of methods you can do that with an "accessor":
public function getPropAAttribute()
{
return $this->getProp() . '_A';
}
Now you can access the propA property of the model:
$model->propA;
Laravel 8.X Docs - Eloquent - Accessors and Mutators - Defining an Accessor
I'm sure there is a common pattern for this kind of thing, and I'm struggling with search terms to find answers, so please bear with me if is this a dupe.
I have a few Classes in my app that create pretty standard Models that are stored in a relational database, eg;
// AtsType::name examples = 'XML', 'RSS', 'SOAP'
class AtsType extends Model
{
public function ats_instances()
{
return $this->hasMany('App\AtsInstance');
}
public function import()
{
}
}
What I need that import() method to do, however, somehow invokes a class/interface/contract/whatever based upon the actual model instance. So something like this;
AtsTypeRss::import()
AtsTypeXml::import()
AtsTypeSoap::import()
I'd like them to be standalone classes, in order to eventually use some artisan commands that will generate them for a developer, along with a data migration to create the new model names into the database.
I'm just unsure how to go about this.
You could try something like (as seen here), I've searched how to use variable in namespace :
class AtsType extends Model
{
protected $import_method = 'MyMethod';
public function ats_instances()
{
return $this->hasMany('App\AtsInstance');
}
public function import()
{
$string = $this->import_method;
$class = '\\controller\\' . $string;
$newObject = new $class();
}
}
I have a BLOB field in my database which contains compressed data.
I need compress / uncompress to be transparent, and user class do not need to write:
$objModel->field = gzencode($objModel->field);
$objModel->field = gzdecode($objModel->field);
For saving I got it, overriding save method:
public function save($attributes[] = null) {
$this->field = gzencode($objModel->field);
return parent::save($attributes);
}
But when I recover data from the database I do not get to gzdecode "transparent", I have tried overriding boot, __call, __callstatic and others, but unsuccessfully.
Can someone tell me which method recovers data from DB and fills the model object so I can override it and make gzdecode?
I wouldn't recommend you override Eloquent methods. Just use accessor:
public function getFieldAttribute($value)
{
return gzdecode($value);
}
And mutator:
public function setFieldAttribute($value)
{
$this->attributes['field'] = gzencode($value);
}
Usually to eager load a relationship I would do something like this:
Model::with('foo', 'bar', 'baz')...
A solution might be to set $with = ['foo','bar','baz'] however that will always load these three relations whenever I call Model
Is it possible to do something like this: Model::with('*')?
No it's not, at least not without some additional work, because your model doesn't know which relations it supports until they are actually loaded.
I had this problem in one of my own Laravel packages. There is no way to get a list of the relations of a model with Laravel. It's pretty obvious though if you look at how they are defined. Simple functions which return a Relation object. You can't even get the return type of a function with php's reflection classes, so there is no way to distinguish between a relation function and any other function.
What you can do to make it easier is defining a function that adds all the relationships.
To do this you can use eloquents query scopes (Thanks to Jarek Tkaczyk for mentioning it in the comments).
public function scopeWithAll($query)
{
$query->with('foo', 'bar', 'baz');
}
Using scopes instead of static functions allows you to not only use your function directly on the model but for example also when chaining query builder methods like where in any order:
Model::where('something', 'Lorem ipsum dolor')->withAll()->where('somethingelse', '>', 10)->get();
Alternatives to get supported relations
Although Laravel does not support something like that out of the box you can allways add it yourself.
Annotations
I used annotations to determine if a function is a relation or not in my package mentioned above. Annotations are not officially part of php but a lot of people use doc blocks to simulate them.
Laravel 5 is going to use annotations in its route definitions too so I figuered it not to be bad practice in this case. The advantage is, that you don't need to maintain a seperate list of supported relations.
Add an annotation to each of your relations:
/**
* #Relation
*/
public function foo()
{
return $this->belongsTo('Foo');
}
And write a function that parses the doc blocks of all methods in the model and returns the name. You can do this in a model or in a parent class:
public static function getSupportedRelations()
{
$relations = [];
$reflextionClass = new ReflectionClass(get_called_class());
foreach($reflextionClass->getMethods() as $method)
{
$doc = $method->getDocComment();
if($doc && strpos($doc, '#Relation') !== false)
{
$relations[] = $method->getName();
}
}
return $relations;
}
And then just use them in your withAll function:
public function scopeWithAll($query)
{
$query->with($this->getSupportedRelations());
}
Some like annotations in php and some don't. I like it for this simple use case.
Array of supported relations
You can also maintain an array of all the supported relations. This however needs you to always sync it with the available relations which, especially if there are multiple developers involved, is not allways that easy.
protected $supportedRelations = ['foo','bar', 'baz'];
And then just use them in your withAll function:
public function scopeWithAll($query)
{
return $query->with($this->supportedRelations);
}
You can of course also override with like lukasgeiter mentioned in his answer. This seems cleaner than using withAll. If you use annotations or a config array however is a matter of opinion.
There's no way to know what all the relations are without specifying them yourself. How the other answers posted are good, but I wanted to add a few things.
Base Model
I kind of have the feeling that you want to do this in multiple models, so at first I'd create a BaseModel if you haven't already.
class BaseModel extends Eloquent {
public $allRelations = array();
}
"Config" array
Instead of hard coding the relationships into a method I suggest you use a member variable. As you can see above I already added $allRelations. Be aware that you can't name it $relations since Laravel already uses that internally.
Override with()
Since you wanted with(*) you can do that too. Add this to the BaseModel
public static function with($relations){
$instance = new static;
if($relations == '*'){
$relations = $instance->allRelations;
}
else if(is_string($relations)){
$relations = func_get_args();
}
return $instance->newQuery()->with($relations);
}
(By the way, some parts of this function come from the original Model class)
Usage
class MyModel extends BaseModel {
public $allRelations = array('foo', 'bar');
}
MyModel::with('*')->get();
I wouldn't use static methods like suggested since... it's Eloquent ;)
Just leverage what it already offers - a scope.
Of course it won't do it for you (the main question), however this is definitely the way to go:
// SomeModel
public function scopeWithAll($query)
{
$query->with([ ... all relations here ... ]);
// or store them in protected variable - whatever you prefer
// the latter would be the way if you want to have the method
// in your BaseModel. Then simply define it as [] there and use:
// $query->with($this->allRelations);
}
This way you're free to use this as you like:
// static-like
SomeModel::withAll()->get();
// dynamically on the eloquent Builder
SomeModel::query()->withAll()->get();
SomeModel::where('something', 'some value')->withAll()->get();
Also, in fact you can let Eloquent do it for you, just like Doctrine does - using doctrine/annotations and DocBlocks. You could do something like this:
// SomeModel
/**
* #Eloquent\Relation
*/
public function someRelation()
{
return $this->hasMany(..);
}
It's a bit too long story to include it here, so learn how it works: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html
Since i've met with a similar problem, and found a good solution that isn't described here and doesn't require filling some custom arrays or whatever, i'll post it for the future.
What i do, is first create a trait, called RelationsManager:
trait RelationsManager
{
protected static $relationsList = [];
protected static $relationsInitialized = false;
protected static $relationClasses = [
HasOne::class,
HasMany::class,
BelongsTo::class,
BelongsToMany::class
];
public static function getAllRelations($type = null) : array
{
if (!self::$relationsInitialized) {
self::initAllRelations();
}
return $type ? (self::$relationsList[$type] ?? []) : self::$relationsList;
}
protected static function initAllRelations()
{
self::$relationsInitialized = true;
$reflect = new ReflectionClass(static::class);
foreach($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
/** #var ReflectionMethod $method */
if ($method->hasReturnType() && in_array((string)$method->getReturnType(), self::$relationClasses)) {
self::$relationsList[(string)$method->getReturnType()][] = $method->getName();
}
}
}
public static function withAll() : Builder
{
$relations = array_flatten(static::getAllRelations());
return $relations ? self::with($relations) : self::query();
}
}
Now you can use it with any class, like -
class Project extends Model
{
use RelationsManager;
//... some relations
}
and then when you need to fetch them from the database:
$projects = Project::withAll()->get();
Some notes - my example relation classes list doesn't include morph relations, so if you want to get them as well - you need to add them to $relationClasses variable. Also, this solution only works with PHP 7.
You could attempt to detect the methods specific to your model using reflection, such as:
$base_methods = get_class_methods('Illuminate\Database\Eloquent\Model');
$model_methods = get_class_methods(get_class($entry));
$maybe_relations = array_diff($model_methods, $base_methods);
dd($maybe_relations);
Then attempt to load each in a well-controlled try/catch. The Model class of Laravel has a load and a loadMissing methods for eager loading.
See the api reference.
You can create method in your Model
public static function withAllRelations() {
return static::with('foo', 'bar', 'baz');
}
And call Model::withAllRelations()
Or
$instance->withAllRelations()->first(); // or ->get()
You can't have a dynamic loading of relationships for a certain model. you need to tell the model which relations to support.
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
The Best Solution
first create a trait, called RelationsManager:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use ReflectionClass;
use ReflectionMethod;
trait RelationsManager
{
protected static $relationsList = [];
protected static $relationsInitialized = false;
protected static $relationClasses = [
HasOne::class,
HasMany::class,
BelongsTo::class,
BelongsToMany::class,
HasOneThrough::class,
HasManyThrough::class,
MorphTo::class,
MorphOne::class,
MorphMany::class,
MorphToMany::class,
];
public static function getAllRelations($type = null): array
{
if (!self::$relationsInitialized) {
self::initAllRelations();
}
return $type ? (self::$relationsList[$type] ?? []) : self::$relationsList;
}
protected static function initAllRelations()
{
self::$relationsInitialized = true;
$reflect = new ReflectionClass(static::class);
foreach ($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
/** #var ReflectionMethod $method */
if ($method->hasReturnType() && in_array((string) $method->getReturnType(), self::$relationClasses)) {
self::$relationsList[(string) $method->getReturnType()][] = $method->getName();
}
}
}
public static function withAll(): Builder
{
$relations = array_flatten(static::getAllRelations());
return $relations ? self::with($relations) : self::query();
}
}
Now you can use it with any class, like -
class Company extends Model
{
use RelationsManager;
//... some relations
}
and then when you need to fetch them from the database:
$companies = Company::withAll()->get();
this solution only works with PHP 7 Or Higher.
Done