Create Command For Laravel Observers

Mohammed Osama
10 min readApr 5, 2018

--

Avoid Copying & Pasting the same file over and over again by creating your custom command

You can always create your own custom artisan command using the make:command as demonstrated here , If you don’t know what are observers and how powerful they are, I totally recommend you to read this article

We have discussed there that unfortunately laravel doesn’t support us with a command to generate observers for us, so why not to create our custom command to be in charge of this process ? , Al-right Let’s do it !

The very first thing to do is running this command

php artisan make:command ObserversCommand

That’s going to create a new command file for us named ObserversCommand in /app/Console/Commands Directory

It should be similar to this

<?phpnamespace App\Console\Commands;use Illuminate\Console\Command;class ObserversCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:name';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//
}
}

We will have to update the $signature property
with the following.

protected $signature = 'make:observer {name : name of the observer.} {--class= : name of the model} {except?* : generate all Eloquent events except for specific ones.}';

and that means in essence, that our command signature consists of the follwoing

  • make:observer // name of the command

so we can call it like the following

php artisan make:observer
  • name // name of the observer

name is an expected argument to be passed whenever calling this command.

whatever after the colon is the description of this parameter and It’s required as it doesn’t have question mark after the name

  • class // name of the model

this argument is an optional one, once the user supplies it, we will use it to create Eloquent events as methods in our class and pass to it parameter as a dependency injection of the model that user mentions out.

  • except

this argument is also optional, and it accepts an array which means that the user can exclude unnecessary events,

In the end the user will be calling the command in the following formula.

php artisan make:observer Post --class=Post retrieved created

and that means create me an PostObserver file under the /app/Observers directory, exclude from it the retrieved and created methods . Also, inject the Post Model to this Observer to restrict this Observer to Its model.

so the output will be similar to the following.

<?php
namespace \App\Observers;
use \App\Post;class PostObserver {
public function creating(Post $post){

}
public function updating(Post $post){

}
public function updated(Post $post){

}
public function saving(Post $post){

}
public function saved(Post $post){

}
public function restoring(Post $post){

}
public function restored(Post $post){

}
public function deleting(Post $post){

}
public function deleted(Post $post){

}
public function forceDeleted(Post $post){

}
}
?>

of course if you don’t exclude anything, all of the methods will be listed out there.

php artisan make:observer Post --class=Post

and that should output the following .

<?php
namespace \App\Observers;
use \App\Post;class PostObserver { // retrieved exists now
public function retrieved(Post $post){

}
public function creating(Post $post){

}
// created exists now
public function created(Post $post){

}
public function updating(Post $post){

}
public function updated(Post $post){

}
public function saving(Post $post){

}
public function saved(Post $post){

}
public function restoring(Post $post){

}
public function restored(Post $post){

}
public function deleting(Post $post){

}
public function deleted(Post $post){

}
public function forceDeleted(Post $post){

}
}
?>

If you don’t want to restrict this observer, which I don’t recommend , just don’t pass the class option

php artisan make:observer Post

The output will be the following then.

<?php
namespace \App\Observers;
class PostObserver {
public function retrieved(){

}
public function creating(){

}
public function created(){

}
public function updating(){

}
public function updated(){

}
public function saving(){

}
public function saved(){

}
public function restoring(){

}
public function restored(){

}
public function deleting(){

}
public function deleted(){

}
public function forceDeleted(){

}
}
?>

Well said, You have already understood what we are about to build, nice and simple command to manage Observer process in a second, Coding Time !

1- Import Filesystem namespace

use Illuminate\Filesystem\Filesystem;

2- Go to constructor and dependency inject Filesystem.

public function __construct(Filesystem $filesystem) {
parent::__construct();
$this->filesystem = $filesystem;
}

We will be using this filesystem in order to create files, inject a piece of codes into these files , do a couple of checks such as checking whether the file exists or not.

3- Update description property

protected $description = 'Create a new observer class';

4- Let’s dig into the handle method

4.1 —Directory Existence.

We will be checking here if the directory exists or not, and If it doesn’t exist we will just create one

public function handle() {
if (!$this->filesystem->isDirectory(app_path('Observers'))) {
$this->filesystem->makeDirectory(app_path('Observers'), 0755, false, true);
$this->info('Observers folder generated !');
}
}

Normally, Observers folder lays within the app Folder,

so we will go to the app path using the function mentioned, check if the directory exists using isDirectory method and pass to it the directory location, if it doesn’t exist, we will just create it using the makeDirectory method. we will pass the permission , recursive & force parameters.

then we flash a message to the user that Observer folder is generated.

4.2 — File Existence.

Remember , we asked the user earlier for the name of the file, we will be using it right now to check if the file exists in our directory or not,

we will be getting the name of the file using $this->argument(‘name’)

as that’s an argument and It’s required.

if (!$this->filesystem->exists(app_path('Observers'. DIRECTORY_SEPARATOR . $this->argument('name') . 'Observer.php'))) {
// file doesn't exist, Let's do something here.
$this->info('Observer Created !');
}else{
$this->info('Observer Already Exists ! , Wake up you need some caffeine :)');
}

4.3 — Model Option Existence.

Inside the If statement, we will be checking if the option exists using $this->option(‘class’)

$hasModel    = trim($this->option('class')) != ''?trim($this->option('class')):null;

We are checking now if the user has passed the option class ( you can rename it whatever, just once you name it in the signature, override it at all places we are using class name)

if the option doesn’t exist, just return us null

If the option really exists, we will do a couple of things such as

Instantiating the model, and get the Observable Eloquent events , so whenever you create your own event , we will also include it within our command, also we will lower case the model name and store it in another variable to create a variable in front of the dependency injection , which will be something like this

public function retrieved (Post $post)

to do so, the code will be like this.

if ($hasModel) {
$model = "\\App\\". ucfirst($hasModel);
$model = new $model;
$model = $model->getObservableEvents();
$loweredCaseModel = lcfirst($this->option('class'));
}

$model->getObservableEvents will return you this.

return array_merge(
[
'retrieved', 'creating', 'created', 'updating', 'updated','saving', 'saved', 'restoring', 'restored','deleting', 'deleted', 'forceDeleted',
],
$this->observables
);
// it will merge the basic events, and your events if exists using array merge.

If the user doesn’t supply a model , we won’t be able to get the eloquent events, which means we will run into the else block that will assign the $model variable to default which is basic event that any model should supply

so we will be creating a property called events

protected $events = [
'retrieved', 'creating', 'created', 'updating', 'updated',
'saving', 'saved', 'restoring', 'restored',
'deleting', 'deleted', 'forceDeleted',
];

in the else block , we will assign this one to $model variable

$model = $this->events;

Here is what we reached till now in this step.

$hasModel    = trim($this->option('class')) != ''?trim($this->option('class')):null;
if ($hasModel) {
$model = "\\App\\". ucfirst($hasModel);
$model = new $model;
$model = $model->getObservableEvents();
$loweredCaseModel = lcfirst($this->option('class'));
}else{
$model = $this->events;
}

4.3 — Looping through the events and create methods with their names in string format

We will do that in order to put this content into the file we created earlier.

I’ll initialize a variable with an empty string called fileContent

$fileContent = ''; 

Let’s then loop through the events and check if the user has passed anything to exclude from our methods, but Let’s focus here for a minute.
What if the method we will be creating in a string format, the user doesn't supply us also with the model name, we will have to create a method without passing any argument to it to be like this

public function retrieved()

unless that , the method should be created like this

public function retrieved(Post $post) // if model name is Post

Hopefully That’s straight forward.
now in order to do that, we will have to use conditions, also I’ll be using heredoc to avoid escaping quotations and injecting variables surrounded by quotations or concatenating , that’s just way cleaner. But, There will be a problem that everything injected there will be either variables or string.
I can’t use conditions , or loops etc..

as mentioned above, we already have to check if the user supplies us with model, also we will have to loop through the events , which makes it a real tie right here.
I’ll circumvent this obstacle by creating an if variable, and the fileContent variable we created earlier to use them to overcome this tie here.

fileContent variable will be concatenated with the methods, and if variable will do the if statement job by creating a callback there.

here is the syntax of if variable

$if = function ($condition, $applied, $rejected) {
return $condition?$applied:$rejected;
};

so I can call that in variable format, and it just does the same work of regular if condition.

$if($condition, $applied, $rejected);

now Let’s loop through the model variable we made earlier.

foreach ($model as $event) {
if (in_array($event, $this->argument('except'))) {
continue;
}
$fileContent .= <<<Event

public function {$event}({$if($hasModel, "$hasModel", '')} {$if($hasModel, "\$$loweredCaseModel", '')}){

}
Event;
}

NOTE : Make Sure that second Event is at the very start of the line, or this will spit out an error.

now we are concatenating the empty string fileContent we created earlier, by this heredoc which contains a string that will just create for each event a method with name of the event and if the user supplies us with the model, we will pass it as an argument, then use the lowered case model variable as variable of this model

4.4 — Creating The class format

after finishing the loop and creating the methods that user doesn’t exclude, we will have to put this content into the class,
I’ll replace the fileContent variable with another heredoc,
it will contain of the following

$fileContent = <<<content
<?php
namespace \App\Observers;
{$if(isset($hasModel) && !empty($hasModel), "use \App\\$hasModel;", '')}class {$this->argument('name')}Observer {
{$fileContent}
}
?>
content;

basically, the content now will contain of the start and end tag of php, namespace of the file. Then if the user supplies us with the model, I’ll import the namespace of it, Then I’ll create a class named with what the user named earlier and concatenate it with Observer, then put the fileContent variable which contains the methods we early made.

Take a break here and die and dump here to check how It is

dd($fileContent);// php artisan make:observer Comment --class=Comment// sample<?php\n
namespace \App\Observers;\n
\n
use \App\Comment;\n
\n
class CommentObserver {\n
public function retrieved(Comment $comment){\n
\n
}\n
public function creating(Comment $comment){\n
\n
}\n
public function created(Comment $comment){\n
\n
}\n
public function updating(Comment $comment){\n
\n
}\n
public function updated(Comment $comment){\n
\n
}\n
public function saving(Comment $comment){\n
\n
}\n
public function saved(Comment $comment){\n
\n
}\n
public function restoring(Comment $comment){\n
\n
}\n
public function restored(Comment $comment){\n
\n
}\n
public function deleting(Comment $comment){\n
\n
}\n
public function deleted(Comment $comment){\n
\n
}\n
public function forceDeleted(Comment $comment){\n
\n
}\n
\n
}\n
?>

4.5 — Putting the content into the created file.

Now we will just use the filesystem again in order to put the content we just created into the file we created earlier at the very start of the command.

$this->filesystem->put(app_path('Observers'. DIRECTORY_SEPARATOR . $this->argument('name') . 'Observer.php'), $fileContent);
$this->info('Observer Created !');

we are putting the content right now into the /app/Observers folder, then concatenate that with the name that user supplied and concatenate it again with Observer.php string, finally pass the second parameter to the method which is the file content.
At last, flash a message that file is created.

here is what we have done so far at once.

<?phpnamespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;class ObserversCommand extends Command {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:observer {name : name of the observer.} {--class= : name of the model} {except?* : generate all Eloquent events except for specific ones.}';
protected $events = [
'retrieved', 'creating', 'created', 'updating', 'updated',
'saving', 'saved', 'restoring', 'restored',
'deleting', 'deleted', 'forceDeleted',
];
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new observer class';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(Filesystem $filesystem) {
parent::__construct();
$this->filesystem = $filesystem;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle() {
if (!$this->filesystem->isDirectory(app_path('Observers'))) {
$this->filesystem->makeDirectory(app_path('Observers'), 0755, false, true);
$this->info('Observers folder generated !');
}
if (!$this->filesystem->exists(app_path('Observers'. DIRECTORY_SEPARATOR . $this->argument('name') . 'Observer.php'))) {
$hasModel = trim($this->option('class')) != ''?trim($this->option('class')):null;
if ($hasModel) {
$model = "\\App\\". ucfirst($hasModel);
$model = new $model;
$model = $model->getObservableEvents();
$loweredCaseModel = lcfirst($this->option('class'));
}else{
$model = $this->events;
}
$fileContent = '';
$if = function ($condition, $applied, $rejected) {
return $condition?$applied:$rejected;
};
foreach ($model as $event) {
if (in_array($event, $this->argument('except'))) {
continue;
}
$fileContent .= <<<Event
public function {$event}({$if($hasModel, "$hasModel", '')} {$if($hasModel, "\$$loweredCaseModel", '')}){

}
Event;}
$fileContent = <<<content
<?php
namespace \App\Observers;
{$if(isset($hasModel) && !empty($hasModel), "use \App\\$hasModel;", '')}class {$this->argument('name')}Observer {
{$fileContent}
}
?>
content;
$this->filesystem->put(app_path('Observers'. DIRECTORY_SEPARATOR . $this->argument('name') . 'Observer.php'), $fileContent);
$this->info('Observer Created !');
}else{
$this->info('Observer Already Exists ! , Wake up you need some caffeine :)');
}
}
}

Still Here ? , I hope that you enjoyed this article and It’s clarified enough to you.

Just don’t forget to follow me to get notified with articles that I create :)

--

--