In the previous article, Giuliano showed us how to leverage PHP’s Magic Methods and Late Biding to create a powerful architecture.
This, however, is neither a secret nor a new cutting-edge OOP pattern.
This same system has been used in Laravel since version 4.0* albeit in the more PHP plain way, using call_user_func_array
(Laravel Model Class).
The ForwardsCalls
trait has been present since version 5.7 and if you read the first part of this series you are already familiar with the concept of using this method to simplify your development. Pretty cool, right?
The devs working on the Laravel Framework thought the exact same thing (I suppose), and created the ForwardsCalls
trait.
NOTE
In this article we will reference Laravel 9.x, but the principles should be valid for all the versions that implement theForwardsCalls
trait.
Table of contents
Everyone's using it!
One of the first instances where every developer using Laravel takes advantage of this approach is when they query their database through a Model
.
Look at this line of code extracted from a class called ProjectController
:
$projects = Project::orderByDesc('id')->get();
Pretty ordinary, right?
If you would dive deeper in the orderByDesc
method, where would you go looking?
Probably inside the Project
class.
Now, if you have some experience with Laravel, you already know this Project
class is almost empty because it inherits all of its properties and methods from the Model
abstract class.
Inside the Model
class we go!
The Model Class
Once here we can see that this abstract class implements a lot of interfaces and uses a good number of traits, but we are looking for a specific method: orderByDesc
.
So we open the find window, type our string and find… nothing.
The method is not defined in this class. We can search inside every trait but without results. Seems like the orderByDesc
method is non-existent.
Every good developer should now think: how is this possible?
The answer lies in the ForwardsCalls
trait, PHP Magic Methods (__call
and __callStatic
), and in the Late Binding!
In the first part of this series, Giuliano did a great job explaining how to re-create this pattern with a couple of classes in plain PHP, so I will not repeat what he already said.
Long story short: the Model
class makes use of the ForwardsCalls
trait (you can see that just by opening the class).
If you start to explore this class you will find both the __call
and __callStatic
methods and see the __callStatic
method do a Late Binding to invoke the requested method as non-static:
return (new static)->$method(...$parameters);
This way every static method not defined inside the Model
class will be redirected to a non-static method (still not defined in this class) and will trigger the __call
magic method.
Inside the __call
magic method we can see a couple of if-blocks, but what we are looking for is the last line:
return $this->forwardCallTo($this->newQuery(), $method, $parameters);
What’s going on here?
The ForwardsCalls trait
The previous code is calling the forwardCallTo
method, defined inside the ForwardsCalls
trait (what a surprise!), passing along 3 parameters:
- the first is a
Query Builder
instance (where theorderByDesc
method is defined!) - the second is the name of the method we invoked (again,
orderByDesc
) - the third is the parameters we used (
'id'
in the previous example)
Now is the right moment to ask: why didn’t the developers keep using the more straightforward call_user_func_array
method?
There are a good number of reasons understandable by looking at the ForwardsCalls
trait:
- the framework has better exception management (look at how the exceptions are handled inside this trait)
- the framework has more flexibility (look at both
forwardCallTo
andforwardDecoratedCallTo
methods) - the developers avoid code repetition inside every class (they just need to use the trait and write a couple of lines for the magic methods)
- and more...
This is a great example of a developer-friendly pattern 🙂
Digging deeper
How could we take advantage of this pattern?
It's time to put our hands on some code!
Just imagine this scenario: you are developing an e-commerce and now you have the need to export your data in various formats (i.e. XML and CSV).
Because you don't need a UI to get these exports, you will create an Artisan Command to run your code right from the terminal.
Pre-requisite: ExportData - part one
Don't be scared if you are not familiar with the creation of Artisan Commands: the code is super simple and you don't need to understand it to follow this tutorial.
To keep things even more simple our export classes will not really export data.
NOTE
If you want to learn about Artisan Commands check out the corresponding section inside the Laravel documentation
To create a new command we just need to run:
php artisan make:command ExportData
This command will create the ExportData
class inside app\Console\Commands
.
Looking at the code we can clearly see two properties and the handle
method. Maybe another time I will better explain how Artisan Commands works, but it's not the point of this article.
Quoting directly from the documentation:
After generating your command, you should define appropriate values for the
signature
anddescription
properties of the class. These properties will be used when displaying your command on thelist
screen. Thesignature
property also allows you to define your command's input expectations. Thehandle
method will be called when your command is executed. You may place your command logic in this method.
So let's copy this code for the $signature
property:
protected $signature = 'data:export
{type : The export format: XML, CSV}
{class : The class to export: Product, User}';
And this is for the $description
property:
protected $description = 'Export selected data in the given format';
In the handle
method we will echo
the sentence "it works"
and test everything running the command:
php artisan data:export xml product
If you see it works
in the terminal, great! You just successfully ran your custom command!
Now it's time to create the export logic and make use of the ForwardsCalls
trait.
The Exporter Logic
Our exporter will be structured this way:
- The main
ExportManager
class, taking care of the general logic - The
IExporter
interface, defining how the single exporter is implemented - A couple of classes for the exporter logic:
CsvExporter
andXmlExporter
implementingIExporter
All these classes are placed inside app\Support
(the full project is available on GitHub).
How do they work?
ExportManager
The ExportManager
use the ForwardsCalls
to redirect most of the calls to the dedicated exporter class.
Inside this class, you can see both __call
and __callStatic
magic methods, a couple of static methods (list
and sendExport
), and the private method getExporter
.
If you read the first part of this article's series you are already familiar with how these magic methods work.
The __call
method gets the type and model from the $parameters
array and capitalizes them (i.e. xml
-> Xml
, PRODUCT
-> Product
).
After that it calls the forwardCallTo
method, passing an instance of the selected Exporter, the $method
called by our code, and the $parameters
array (just in case we should need it).
To get the correct Exporter instance (i.e. CsvExporter
, XmlExporter
, etc...) the ExportManager
use the getExporter
method.
This method returns an instance created dynamically from both $type
and $model
parameters.
/**
* Create an exporter instance
*
* @param string $type the Exporter type (i.e. Csv)
* @param string $model the model to use in the export (i.e. Product)
*
* @return mixed an instance of the exporter (i.e. CsvExporter, XmlExporter, etc...)
*/
private function getExporter($type, $model)
{
try {
// $type = Csv -> App\Support\Exporters\CsvExoporter
$exporter = "App\\Support\\Exporters\\{$type}Exporter";
// $model = Product -> App\Models\Product
$data = "App\\Models\\{$model}";
// new App\Support\Exporters\CsvExoporter(App\Models\Product)
return new $exporter($data);
} catch (Error | Exception $e) {
throw $e;
}
}
This way we get the instance of the export, initialized with the model it will use to get the data from the database.
The ExportManager
class act both as a manager for the exports (it can list them or send a particular report to the administrator) and as a switch to call the class needed for the selected type of export (using the Dynamic Class Instantiation).
IExporter, CsvExporter, XmlExporter
The IExporter
interface allows us to create code that specifies which methods the exporters must implement, without having to define how these methods are implemented:
interface IExporter
{
/**
* Get the data from the model and prepare the export
*/
public function prepareExport();
/**
* Create the export
*/
public function export();
/**
* Save the export inside the database
*/
public function save();
}
Using an interface
we define the basic standard for all the exporters we will create, allowing us to create objects of different classes that may be used interchangeably because they implement the same interface.
This is what we want for all our current and future exporters.
Now let's take a look at one of the exporters (for the sake of simplicity both the exporters do almost the same things, but of course, they can do different operations).
class CsvExporter implements IExporter
{
public static $filetype = "CSV";
private $items;
private $model;
public $lastExportId;
public function __construct($_model)
{
$this->model = $_model;
}
public function prepareExport()
{
$this->items = $this->model::all();
echo "Preparing export of " . get_class(new $this->model);
echo "\n";
}
public function export()
{
$this->prepareExport();
echo "{$this->items->count()} records exported in " . static::$filetype . " format";
echo "\n";
return $this;
}
public function save()
{
$this->lastExportId = rand(1, 1305);
echo "Export n. {$this->lastExportId} saved inside database";
echo "\n";
return $this;
}
}
Feel free to download the example project and add your custom exporter! Get your hands dirty it's a great way to wrap your head around this kind of pattern.
Remember to create a DB and run all the migrations and seeder before running the
data:export
command.
Basically, every exporter is instantiated with a model, use this model to get all the records and simulate a DB writing.
The export
and save
methods return an instance of the export class so that we can chain the methods calls and also get the instance in our DataExport
class.
NOTE
We get the exporter instance (CsvExporter
,XmlExporter
, etc...) because we are using theforwardCallTo
method inside__call
in ourExportManager
class.If we had used the
forwardDecoratedCallTo
method, we would have got an instance ofExportManager
. Try it!
With all this logic out of the way we have only one more thing to do: add the correct code in the DataExport
handle
method.
ExportData - part two
Remember our handle
method that does nothing more than echo "it works"
? It's time to make it work for real!
We need to import the ExportManager
class by writing use App\Support\ExportManager;
before the class definition.
In the handle
method we can now call the export
method on the ExportManager
(even if it's defined on the specific exporter) and get the correct instance.
With this instance we can then call sendReport
, pass it the ID of the export we just created, and have it sent to the administrator inbox.
public function handle()
{
$export = ExportManager::export($this->argument('type'), $this->argument('class'))->save();
ExportManager::sendExport($export->lastExportId);
}
Run the command
Finally, you can test if everything it's working by running this command:
php artisan export:data csv product
Our data is exported in the format we want! Yay!
The logic we implemented allowed us to create a very flexible exporter: we can export all the Models
defined in our Laravel application and should we need a different kind of exporter (i.e. PdfExporter
, ExcelExporter
, etc...) we just need to create a new class caring only about the exporting logic and nothing more!
In conclusion
It was a wild ride, wasn't it?
I hope this helped you better understand one of the gems hidden inside the Laravel Framework!
The ForwardsCalls
trait gives us a powerful pattern to leverage when developing inside a Laravel application. It gives us a lot of flexibility and the possibility to keep our classes clean, well defined but still able to communicate.
Giuliano also showed us how to bring this pattern outside Laravel and use it virtually in every PHP project we want.
I enjoyed writing this article's series with Giuliano and I really hope this article inspired you to dig a little bit inside the code of the libraries and frameworks you use.
Here you can find the first half made by Giuliano. ;)
If this article was helpful or want to start a conversation, feel free to reach out in the comments or here @gosty93 and @donato-riccio-wda
We'll be happy to receive any feedback or ideas for future articles and tutorials.
Happy Coding | _ 0
- This is the earliest version you could find on their public repository
Top comments (0)