I want to develop a plugin that extends RainLab.Blog in October CMS.
My plugin's database appears in the SQL database and backend form that takes the new input while editing a blog post renders just fine, but the issue appears when trying to save the changes to SQL, wheree they appear NULL:
"Method OleMol\BlogExtensions\Models\Extension::__toString() must not throw an exception, caught Illuminate\Database\Eloquent\JsonEncodingException: Error encoding model [OleMol\BlogExtensions\Models\Extension] with ID [2] to JSON: Recursion detected" on line 0 of C:\xampp\htdocs\vendor\laravel\framework\src\Illuminate\Support\Str.php
This seems to be a Laravel problem and it's the first time I'm using October in a professional project.
Initially, I tried to use a table with a bunch of columns, but for troubleshooting, I have stripped it down to one custom column until I can find an answer.
I am trying to follow along to this video tutorial, but extending RainLab.Blog instead of RainLab.User.
cd C:\xampp\htdocs
php artisan create:plugin OleMol.BlogExtensions
plugins\olemol\blogextensions\Plugin.php
<?php namespace OleMol\BlogExtensions;
use Backend;
use System\Classes\PluginBase;
use RainLab\Blog\Models\Post as PostModel;
use RainLab\Blog\Controllers\Posts as PostsController;
use OleMol\BlogExtensions\Models\Extension as ExtensionModel;
/**
* BlogExtensions Plugin Information File
*/
class Plugin extends PluginBase {
/* RainLab.Blog is a dependency for this plugin. */
public $require = [ 'RainLab.Blog' ];
public function boot() {
PostModel::extend(function($model) {
$model -> hasOne[ 'blogextension' ] = [ 'OleMol\BlogExtensions\Models\Extension' ];
});
PostsController::extendFormFields(function($form, $model, $context) {
//Only extend this controller if the model is a post.
if (!$model instanceof PostModel) { return; }
//Don't create an extension to a model that does not exist.
if (!$model -> exists) { return; }
//Ensure that the post always has an extension before adding fields.
ExtensionModel::getFromPost($model);
$form -> addTabFields([ 'blogextension[business_name ]' => [ 'label' => 'Business Name', 'tab' => 'Business' ]);
});
}
} }
plugins\olemol\blogextensions\updates\version.yaml
1.0.1:
- First version of BlogExtensions
1.0.2:
- create_business_table.php
plugins\olemol\blogextensions\updates\create_business_table.php
<?php namespace OleMol\BlogExtensions\Updates;
use Schema;
use October\Rain\Database\Schema\Blueprint;
use October\Rain\Database\Updates\Migration;
class CreateBusinessTable extends Migration {
public function up() {
Schema::create('olemol_blogextensions_business', function (Blueprint $table) {
$table -> engine = 'InnoDB';
$table -> increments('id');
$table -> integer('post_id') -> unsigned() -> index();
/*
* This always ends up as NULL!
* (Or default value in case it's not nullable).
*/
$table -> string('business_name') -> nullable();
$table -> timestamps();
});
}
public function down() {
Schema::dropIfExists('olemol_blogextensions_business');
}
}
plugins\olemol\blogextensions\models\Extension.php
<?php namespace OleMol\BlogExtensions\Models;
use Model;
/**
* Extension Model
*/
class Extension extends Model {
use \October\Rain\Database\Traits\Validation;
/**
* #var string table associated with the model
*/
public $table = 'olemol_blogextensions_business';
/**
* #var array dates attributes that should be mutated to dates
*/
protected $dates = [ 'created_at', 'updated_at' ];
public $belongsTo = [ 'post' => [ 'RainLab\Blog\Models\Post' ]];
/* Helper-function to ensure that post always has an extension. */
public static function getFromPost($post) {
if ($post -> extension) { return $post -> extension; }
$extension = new static;
$extension -> post = $post;
$extension -> save();
$post -> extension = $extension;
return $extension;
}
}
I have included the code I think is necessary. The files still contain auto-generated empty blocks which are omitted.
php artisan plugin:refresh OleMol.BlogExtensions
php artisan project:sync
php artisan october:migrate
I'm on a tight schedule and I feel like I'm lost at this point.
Please reach out if more information is needed.
I would appreciate any help coming my way.
I think in your code Extension.php-> getFromPost method need some corrections
// plugins\olemol\blogextensions\models\Extension.php: getFromPost method:
$post->extension = $extension;
It should be: blogextension instead of only extension
$post->blogextension = $extension;
// ^ HERE
You need to specify your hasOne relation name there
if any doubt please comment
Related
After installing the plugin and editing the available fields in the configuration, I would like to save these fields to my entity.
so I created the Config class to make the field getter from the plugin configuration like:
class Config
{
public const SYSTEM_CONFIG_DOMAIN = 'myEntity.config.';
/** #var SystemConfigService $systemConfigService */
protected static $systemConfigService;
public function __construct(SystemConfigService $systemConfigService)
{
self::$systemConfigService = $systemConfigService;
}
}
public static function message(): string
{
return (string)self::$systemConfigService->get(self::SYSTEM_CONFIG_DOMAIN . 'message');
}
I found such a controller in platforms:
/**
* #Route("/api/v{version}/_action/system-config/batch", name="api.action.core.save.system-config.batch", methods={"POST"})
*/
public function batchSaveConfiguration(Request $request): JsonResponse
{
foreach ($request->request->all() as $salesChannelId => $kvs) {
if ($salesChannelId === 'null') {
$salesChannelId = null;
}
$this->saveKeyValues($salesChannelId, $kvs);
}
//save to my entity here
return new JsonResponse([$request->request->all()]);
}
And in this controller I would like to add my message from pluginConfig to my entity.
Is this a good idea and how can I thank it? Could I ask for more code than usual because I'm just learning sw6?
If you just want to add data to your plugin configuration you should use a config installer instead of a controller.
Maybe I'm searching it all wrong but I haven't been able to figure out an answer.. Say I have a model Building, which always has n Floor(s)
I would like to write a constructor for Building, in which I could specify a number of Floor(s) to be created. The problem is that I can't link back a Floor to the Building because when the constructor for Building is being called, it doesn't have a primary key yet...
Basically, my code looks like this but doesn't work:
class Building extends Model {
public function __construct($nbFloors) {
for($i=0; $i<$nbFloors; $i++) {
$foo = new Floor();
$foo->building_id = $this->id;
$foo->save();
}
}
}
What would be the correct solution to achieve something like that?
The primary key will never be available in the constructor and your constructor's definition is not compatible with Model which expects an array of attributes as the first argument.
You're performing too much logic in your constructor, a constructor is meant to just instantiate an object and its dependencies, not perform business logic. By doing this in your constructor, you're actually going to be attempting to create new floors EVERY time your Model is instantiated which includes when your model is retrieved from the database.
I'd recommend adding a new method like:
public function createWithFloors($n) {
$this->save();
...
}
Now, you can use the model as it's expected and call the create method:
$building = new Building(['name' => 'Empire State']);
$building->createWithFloors(102);
Besides the solutions already suggested, you could create an event that is fired when a Building is created. A listener could then store your Floors. For event reference, have a look at the documentation.
First, create an event called BuildingCreated with php artisan make:event BuildingCreated and use below code:
namespace App\Events;
use App\Building;
use Illuminate\Queue\SerializesModels;
class BuildingCreated extends Event
{
use SerializesModels;
public $building;
public function __construct(Building $building)
{
$this->building = $building;
}
}
Then, register the event within your Building model:
use App\Events\BuildingCreated;
class Building
{
protected $dispatchesEvents = [
'created' => BuildingCreated::class,
];
}
Next, you will need a listener that creates the floors. Create it with php artisan make:listener AddFloorsToNewBuilding and adapt it as you need:
namespace App\Listeners;
use App\Building;
use App\Events\BuildingCreated;
class AddFloorsToNewBuilding
{
public function handle(BuildingCreated $event)
{
$floors = ...;
$event->building->floors()->saveMany($floors);
$event->building->save();
}
}
Lastly, have the listener listen for the event by adding it to the $listen array in the EventServiceProvider:
class EventServiceProvider
{
protected $listen = [
\App\Events\BuildingCreated::class => [
\App\Listeners\AddFloorsToNewBuilding::class,
],
];
}
since, you can't bind Floor to a building that is not created yet, you should make the "new floors number" an attribute of the Building instance. Then you overload the save method to create the new floors.
class Building extends Model {
/** number of floors to be created on save
* #var int
*/
private $newFloorsCount;
/**
* Building constructor.
* #param array $attributes
* #param int $nbFloors
*/
public function __construct(array $attributes = [], $nbFloors = 0) {
parent::__construct($attributes);
$this->newFloorsCount = $nbFloors;
}
/**
* #param array $options
* #return bool
*/
public function save(array $options = [])
{
$return = parent::save($options);
for($i=0; $i<$this->newFloorsCount; $i++) {
$foo = new Floor();
$foo->building_id = $this->id;
$foo->save();
}
return $return;
}
}
now you can just do
$building = new Building([],5);
$building->save();
I'm trying to store all my settings from my settings table into a global variable, but I'm stucked now(I have no idea what's the next step), this is my actual model and seeder:
model - Settings.php
class Setting extends Model
{
protected $table = 'settings';
public $timestamps = false;
protected $fillable = [
'name',
'value',
];
}
seeder - SettingsTableSeeder.php
class SettingsTableSeeder extends Seeder
{
public function run()
{
$settings = [
['name' => 'title', 'value' => ''],
['name' => 'facebook', 'value' => ''],
['name' => 'twitter', 'value' => ''],
['name' => 'instagram', 'value' => '']
];
foreach($settings as $setting){
\App\Setting::create($setting);
}
}
}
How can I store all the data inside the settings table and make then acessible from blade, or any controller or view?
Edit
Now, my question is, how can i update a single or multiple value(s) from a form?
I have set this up:
My route:
Route::put('/', ['as' => 'setting.update', 'uses' => 'Admin\AdminConfiguracoesController#update']);
My Admin\AdminConfiguracoesController:
class AdminConfiguracoesController extends AdminBaseController
{
private $repository;
public function __construct(SettingRepository $repository){
$this->repository = $repository;
}
public function geral()
{
return view('admin.pages.admin.configuracoes.geral.index');
}
public function social()
{
return view('admin.pages.admin.configuracoes.social.index');
}
public function analytics()
{
return view('admin.pages.admin.configuracoes.analytics.index');
}
public function update($id, Factory $cache, Setting $setting)
{
$this->repository->findByName($setting);
$cache->forget('settings');
return redirect('admin');
}
}
My SettingRepository:
class SettingRepository
{
private $model;
public function __construct(Setting $model)
{
$this->model = $model;
}
public function findByName($name){
return $this->model->where('name', $name)->update();
}
}
My blade form:
{!! Form::model(config('settings'), ['class' => 's-form', 'route' => ['setting.update']]) !!}
{{ method_field('PUT') }}
<div class="s-form-item text">
<div class="item-title required">Título do artigo</div>
{!! Form::text('title', null, ['placeholder' => 'Nome do site']) !!}
#if($errors->has('title'))
<div class="item-desc">{{ $errors->first('title') }}</div>
#endif
</div>
<div class="s-form-item s-btn-group s-btns-right">
Voltar
<input class="s-btn" type="submit" value="Atualizar">
</div>
{!! Form::close() !!}
But things does not work. How can I update the values into the table?
See improved answer in Update 2
I would add a dedicated Service Provider for this. It will read all your settings stored in the database and add them to Laravels config. This way there is only one database request for the settings and you can access the configuration in all controllers and views like this:
config('settings.facebook');
Step 1: Create the Service Provider.
You can create the Service Provider with artisan:
php artisan make:provider SettingsServiceProvider
This will create the file app/Providers/SettingsServiceProvider.php.
Step 2: Add this to the boot-method of the provider you have just created:
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
// Laravel >= 5.2, use 'lists' instead of 'pluck' for Laravel <= 5.1
config()->set('settings', \App\Setting::pluck('value', 'name')->all());
}
From the Laravel Docs:
[The boot method] is called after all other service providers have been registered, meaning you have access to all other services that have been registered by the framework.
http://laravel.com/docs/5.1/providers#the-boot-method
Step 3: Register the provider in your App.
Add this line to the providers array in config/app.php:
App\Providers\SettingsServiceProvider::class,
And that's it. Happy coding!
Update: I want to add that the boot-method supports dependency injection. So instead of hard coding \App\Setting, you could inject a repository / an interface that is bound to the repository, which is great for testing.
Update 2: As Jeemusu mentioned in his comment, the app will query the database on every request. In order to hinder that, you can cache the settings. There are basically two ways you can do that.
Put the data into the cache every time the admin is updating the
settings.
Just remember the settings in the cache for some time and clear the cache every time the admin updates the settings.
To make thinks more fault tolerant, I'd use the second option. Caches can be cleared unintentionally. The first option will fail on fresh installations as long as the admin did not set the settings or you reinstall after a server crash.
For the second option, change the Service Providers boot-method:
/**
* Bootstrap the application services.
*
* #param \Illuminate\Contracts\Cache\Factory $cache
* #param \App\Setting $settings
*
* #return void
*/
public function boot(Factory $cache, Setting $settings)
{
$settings = $cache->remember('settings', 60, function() use ($settings)
{
// Laravel >= 5.2, use 'lists' instead of 'pluck' for Laravel <= 5.1
return $settings->pluck('value', 'name')->all();
});
config()->set('settings', $settings);
}
Now you only have to make the cache forget the settings key after the admin updates the settings:
/**
* Updates the settings.
*
* #param int $id
* #param \Illuminate\Contracts\Cache\Factory $cache
*
* #return \Illuminate\Http\RedirectResponse
*/
public function update($id, Factory $cache)
{
// ...
// When the settings have been updated, clear the cache for the key 'settings':
$cache->forget('settings');
// E.g., redirect back to the settings index page with a success flash message
return redirect()->route('admin.settings.index')
->with('updated', true);
}
To avoid querying the database on each request, you should save the settings to a config file each time they are changed by the admin/user.
// Grab settings from database as a list
$settings = \App\Setting::lists('value', 'name')->all();
// Generate and save config file
$filePath = config_path() . '/settings.php';
$content = '<?php return ' . var_export($settings, true) . ';';
File::put($filePath, $content);
The above will create a Laraval compatible config file that essentially just returns an array of key => values. The generated file will look something like this.
<?php
return array(
name => 'value',
name => 'value',
);
Any php file in the /config directory will be auto-included by Laravel and the array variables accessible to your application via the config() helper:
config('settings.variable_name');
I want to share my use case, my answer may not be directly answering OP, but hope answering future developers.
This is tested in Laravel 8 application, but i believe it will work fine from Laravel version 5.5 and up.
In my case, I have a settings table with key and value fields as you may see in this migration file.
<?php
use...;
class CreateSettingsTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value');
$table->timestamps();
});
}
//...
}
I heavily use the values stored in this table in my application, so to gain on performance i store those key/value in a config file, I do that anytime the admin has updated a value in the back office.
For that purpose i use Eloquent model events, to be more precise I use saved event, because the saving/saved events will dispatch when a model is created or updated.
<?php
namespace App\Models;
use...;
class Setting extends Model
{
use HasFactory;
//...
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::saved(function () {
$settings = static::pluck('value', 'key')->toArray();
$stringify_settings = var_export($settings, true);
$content = "<?php return {$stringify_settings};";
File::put(config_path('app_settings.php'), $content);
});
}
}
I did one more thing, I added /config/app_settings.php to .gitignore file.
Resources: Store settings table in a configuration file
You can store the data in the database just like you do it normally in Laravel. \App\Setting::create(), \App\Setting::new() and other methods.
For using the values in blade, you can do {{\App\Setting::where('name','title')->pluck('value')}}
And, you can also use scopes for this.
class Setting extends Model
{
public function scopeFor($query, $settingName)
{
return $query->where('name', $settingName);
}
}
then you could use \App\Setting::for('title')->pluck('value')
So the way I see it is that a good Laravel application should be very model- and event-driven.
I have a Model called Article. I wish to send email alerts when the following events happen:
When an Article is created
When an Article is updated
When an Article is deleted
The docs say I can use Model Events and register them within the boot() function of App\Providers\EventServiceProvider.
But this is confusing me because...
What happens when I add further models like Comment or Author that need full sets of all their own Model Events? Will the single boot() function of EventServiceProvider just be absolutely huge?
What is the purpose of Laravel's 'other' Events? Why would I ever need to use them if realistically my events will only respond to Model CRUD actions?
I am a beginner at Laravel, having come from CodeIgniter, so trying to wrap my head around the proper Laravel way of doing things. Thanks for your advice!
In your case, you may also use following approach:
// Put this code in your Article Model
public static function boot() {
parent::boot();
static::created(function($article) {
Event::fire('article.created', $article);
});
static::updated(function($article) {
Event::fire('article.updated', $article);
});
static::deleted(function($article) {
Event::fire('article.deleted', $article);
});
}
Also, you need to register listeners in App\Providers\EventServiceProvider:
protected $listen = [
'article.created' => [
'App\Handlers\Events\ArticleEvents#articleCreated',
],
'article.updated' => [
'App\Handlers\Events\ArticleEvents#articleUpdated',
],
'article.deleted' => [
'App\Handlers\Events\ArticleEvents#articleDeleted',
],
];
Also make sure you have created the handlers in App\Handlers\Events folder/directory to handle that event. For example, article.created handler could be like this:
<?php namespace App\Handlers\Events;
use App\Article;
use App\Services\Email\Mailer; // This one I use to email as a service class
class ArticleEvents {
protected $mailer = null;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function articleCreated(Article $article)
{
// Implement mailer or use laravel mailer directly
$this->mailer->notifyArticleCreated($article);
}
// Other Handlers/Methods...
}
Recently I came to same problem in one of my Laravel 5 project, where I had to log all Model Events. I decided to use Traits. I created ModelEventLogger Trait and simply used in all Model class which needed to be logged. I am going to change it as per your need Which is given below.
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Event;
/**
* Class ModelEventThrower
* #package App\Traits
*
* Automatically throw Add, Update, Delete events of Model.
*/
trait ModelEventThrower {
/**
* Automatically boot with Model, and register Events handler.
*/
protected static function bootModelEventThrower()
{
foreach (static::getModelEvents() as $eventName) {
static::$eventName(function (Model $model) use ($eventName) {
try {
$reflect = new \ReflectionClass($model);
Event::fire(strtolower($reflect->getShortName()).'.'.$eventName, $model);
} catch (\Exception $e) {
return true;
}
});
}
}
/**
* Set the default events to be recorded if the $recordEvents
* property does not exist on the model.
*
* #return array
*/
protected static function getModelEvents()
{
if (isset(static::$recordEvents)) {
return static::$recordEvents;
}
return [
'created',
'updated',
'deleted',
];
}
}
Now you can use this trait in any Model you want to throw events for. In your case in Article Model.
<?php namespace App;
use App\Traits\ModelEventThrower;
use Illuminate\Database\Eloquent\Model;
class Article extends Model {
use ModelEventThrower;
//Just in case you want specific events to be fired for Article model
//uncomment following line of code
// protected static $recordEvents = ['created'];
}
Now in your app/Providers/EventServiceProvider.php, in boot() method register Event Handler for Article.
public function boot(DispatcherContract $events)
{
parent::boot($events);
$events->subscribe('App\Handlers\Events\ArticleEventHandler');
}
Now create Class ArticleEventHandler under app/Handlers/Events directory as below,
<?php namespace App\Handlers\Events;
use App\Article;
class ArticleEventHandler{
/**
* Create the event handler.
*
* #return \App\Handlers\Events\ArticleEventHandler
*/
public function __construct()
{
//
}
/**
* Handle article.created event
*/
public function created(Article $article)
{
//Implement logic
}
/**
* Handle article.updated event
*/
public function updated(Article $article)
{
//Implement logic
}
/**
* Handle article.deleted event
*/
public function deleted(Article $article)
{
//Implement logic
}
/**
* #param $events
*/
public function subscribe($events)
{
$events->listen('article.created',
'App\Handlers\Events\ArticleEventHandler#created');
$events->listen('article.updated',
'App\Handlers\Events\ArticleEventHandler#updated');
$events->listen('article.deleted',
'App\Handlers\Events\ArticleEventHandler#deleted');
}
}
As you can see from different answers, from different Users, there are more than 1 way of handling Model Events. There are also Custom events That can be created in Events folder and can be handled in Handler folder and can be dispatched from different places. I hope it helps.
I found this the cleanest way to do what you want.
1.- Create an observer for the model (ArticleObserver)
use App\Article;
class ArticleObserver{
public function __construct(Article $articles){
$this->articles = $articles
}
public function created(Article $article){
// Do anything you want to do, $article is the newly created article
}
}
2.- Create a new ServiceProvider (ObserversServiceProvider), remember to add it to you config/app.php
use App\Observers\ArticleObserver;
use App\Article;
use Illuminate\Support\ServiceProvider;
class ObserversServiceProvider extends ServiceProvider
{
public function boot()
{
Article::observe($this->app->make(ArticleObserver::class));
}
public function register()
{
$this->app->bindShared(ArticleObserver::class, function()
{
return new ArticleObserver(new Article());
});
}
}
You can opt for the Observer approach to deal with Model Events. For example, here is my BaseObserver:
<?php
namespace App\Observers;
use Illuminate\Database\Eloquent\Model as Eloquent;
class BaseObserver {
public function saving(Eloquent $model) {}
public function saved(Eloquent $model) {}
public function updating(Eloquent $model) {}
public function updated(Eloquent $model) {}
public function creating(Eloquent $model) {}
public function created(Eloquent $model) {}
public function deleting(Eloquent $model) {}
public function deleted(Eloquent $model) {}
public function restoring(Eloquent $model) {}
public function restored(Eloquent $model) {}
}
Now if I am to create a Product Model, its Observer would look like this:
<?php
namespace App\Observers;
use App\Observers\BaseObserver;
class ProductObserver extends BaseObserver {
public function creating(Eloquent $model)
{
$model->author_id = Sentry::getUser()->id;
}
public function created(Eloquent $model)
{
if(Input::hasFile('logo')) Image::make(Input::file('logo')->getRealPath())->save(public_path() ."/gfx/product/logo_{$model->id}.png");
}
public function updating(Eloquent $model)
{
$model->author_id = Sentry::getUser()->id;
}
public function updated(Eloquent $model)
{
if(Input::has('payment_types')) $model->paymentTypes()->attach(Input::get('payment_types'));
//Upload logo
$this->created($model);
}
}
Regarding listeners, I create an observers.php file inside Observers dir and I include it from the AppServiceProvider. Here is a snippet from within the observers.php file:
<?php
\App\Models\Support\Ticket::observe(new \App\Observers\Support\TicketObserver);
\App\Models\Support\TicketReply::observe(new \App\Observers\Support\TicketReplyObserver);
All of this is regarding Model Events.
If you need to send an e-mail after a record is created, it would be cleaner to use the Laravel 'other' Events, as you will have a dedicated class to deal with just that, and fire it, when you wish, from the Controller.
The 'other' Events will have much more purpose as the more automated your app becomes, think of all the daily cronjobs you will need at some point. There will be no more cleaner way to deal with that other than 'other' Events.
You've tagged this question as Laravel 5, so I would suggest not using model events as you'll end up with lots of extra code in your models which may make things difficult to manage in future. Instead, my recommendation would be to make use of the command bus and events.
Here's the docs for those features:
http://laravel.com/docs/5.0/bus
http://laravel.com/docs/5.0/events
My recommendation would be to use the following pattern.
You create a form which submits to your controller.
Your controller dispatches the data from the request generated to a command.
Your command does the heavy lifting - i.e. creates an entry in the database.
Your command then fires an event which can be picked up by an event handler.
Your event handler does something like send an email or update something else.
There are a few reasons why I like this pattern: Conceptually your commands handle things that are happening right now and events handle things that have just happened. Also, you can easily put command and event handlers onto a queue to be processed later on - this is great for sending emails as you tend not to want to do that in real time as they slow the HTTP request down a fair bit. You can also have multiple event handlers for a single event which is great for separating concerns.
It would be difficult to provide any actual code here as your question more about the concepts of Laravel, so I'd recommend viewing these videos so you get a good idea of how this pattern works:
This one describes the command bus:
https://laracasts.com/lessons/laravel-5-events
This one describes how events work:
https://laracasts.com/lessons/laravel-5-commands
You can have multiple listeners on an event. So you may have a listener that sends an email when an article is updated, but you could have a totally different listener that does something totally different—they’ll both be executed.
1) You may create an event listener for each new Model (ArticleEventSubscriber,CommentEventSubscriber) at boot method:
EventServiceProvider.php
public function boot(DispatcherContract $events)
{
parent::boot($events);
$events->subscribe('App\Listeners\ArticleEventListener');
$events->subscribe('App\Listeners\CommentEventListener');
}
or you may also use $subscribe property
protected $subscribe = [
'App\Listeners\ArticleEventListener',
'App\Listeners\CommentEventListener',
];
There are many ways to listen and handle events. Take a look to current master documentation for discovering more ways(like usings closures) to do so : Laravel Docs (master) and this other answer
2) Model events are just events provided by default by Eloquent.
https://github.com/illuminate/database/blob/491d58b5cc4149fa73cf93d499efb292cd11c88d/Eloquent/Model.php#L1171
https://github.com/illuminate/database/blob/491d58b5cc4149fa73cf93d499efb292cd11c88d/Eloquent/Model.php#L1273
I might come after the battle, but If you do not want all the fuss of extending classes or creating traits, you might want to give a try to this file exploration solution.
Laravel 5.X solution
Beware the folder you choose to fetch the models should only contain models to make this solution to work
Do not forget to add the use File
app/Providers/AppServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use File;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$model_location = base_path() . '/app'; // Change to wherever your models are located at
$files = File::files( $model_location );
foreach( $files as $data ) {
$model_name = "App\\" . pathinfo($data)['filename'];
$model_name::creating(function($model) {
// ...
});
$model_name::created(function($model) {
// ...
});
$model_name::updating(function($model) {
// ...
});
$model_name::updated(function($model) {
// ...
});
$model_name::deleting(function($model) {
// ...
});
$model_name::deleted(function($model) {
// ...
});
$model_name::saving(function($model) {
// ...
});
$model_name::saved(function($model) {
// ...
});
}
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
}
Hope it helps you write the less code possible!
Laravel 6, the shortest solution
BaseSubscriber class
namespace App\Listeners;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Str;
/**
* Class BaseSubscriber
* #package App\Listeners
*/
abstract class BaseSubscriber
{
/**
* Returns the first part of an event name (before the first dot)
* Can be a class namespace
* #return string
*/
protected abstract function getEventSubject(): string;
/**
* Register the listeners for the subscriber.
* #param Dispatcher $events
*/
public function subscribe($events)
{
$currentNamespace = get_class($this);
$eventSubject = strtolower(class_basename($this->getEventSubject()));
foreach (get_class_methods($this) as $method) {
if (Str::startsWith($method, 'handle')) {
$suffix = strtolower(Str::after($method, 'handle'));
$events->listen("$eventSubject.$suffix", "$currentNamespace#$method");
}
}
}
}
OrderEventSubscriber class. Handlers for Order model events
use App\Models\Order;
/**
* Class OrderEventSubscriber
* #package App\Listeners
*/
class OrderEventSubscriber extends BaseSubscriber
{
/**
* #return string
*/
protected function getEventSubject(): string
{
return Order::class; // Or just 'order'
}
/**
* #param Order $order
*/
public function handleSaved(Order $order)
{
// Handle 'saved' event
}
/**
* #param Order $order
*/
public function handleCreating(Order $order)
{
// Handle 'creating' event
}
}
ModelEvents trait. It goes to your models, in my case - App\Model\Order
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
/**
* Trait ModelEvents
* #package App\Traits
*/
trait ModelEvents
{
/**
* Register model events
*/
protected static function bootModelEvents()
{
foreach (static::registerModelEvents() as $eventName) {
static::$eventName(function (Model $model) use ($eventName) {
event(strtolower(class_basename(static::class)) . ".$eventName", $model);
});
}
}
/**
* Returns an array of default registered model events
* #return array
*/
protected static function registerModelEvents(): array
{
return [
'created',
'updated',
'deleted',
];
}
}
Register the subscriber in a service provider, e.g AppServiceProvider
/**
* #param Dispatcher $events
*/
public function boot(Dispatcher $events)
{
$events->subscribe(OrderEventSubscriber::class);
}
How just add the ModelEvents trait into your model, adjust the events you want to register instead of default ones:
protected static function registerModelEvents(): array
{
return [
'creating',
'saved',
];
}
Done!
I have it so that when a new branch is created the name gets turned into a slug for lookup, but when I update the record the slug variable stays the same, is there a way to have it automatically update whenever the record is changed?
class Branch extends \Eloquent {
public static $rules = [
'name' => 'required'
];
protected $fillable = ['name', 'slug'];
protected function setNameAttribute($name)
{
$this->attributes['name'] = $name;
$this->attributes['slug'] = Str::slug($name);
}
}
and in my controller...
public function update($slug)
{
$branch = Branch::whereSlug($slug);
$validator = Validator::make($data = Input::except('_method', '_token'), Branch::$rules);
if ($validator->fails())
{
return Redirect::back()->withErrors($validator)->withInput();
}
$branch->update($data);
return Redirect::route('branches.index');
}
First off, the model event should kick in to the mutator if it's the Eloquent model you're dealing with - the Fluent query builder won't. I think that Branch::whereSlug($slug) returns a Fluent query builder - just check to see if Branch::whereSlug($slug)->firstOrFail() works. I think it probably will.
Failing that, there are two options that I'd recommend - first off, Colin Viebrock has an excellent package for generating slugs which is super easy to use.
If you'd rather homebrew it, I'd just put it in the boot method using a model event:
public static function boot() {
static::saving( function( $model ) {
$model->name = $model->name; // force the slug to be rebuilt
} );
}