Symfony: Dynamic route for multiple entities - php

Currently working with Symfony 5.2
I am trying to create a dynamic route for multiple enties which have mostly same properties (some still have other fields too, but all have the same default properties).
Example (minified)
Entity News:
- id, title, author
Entity Event:
- id, title, author
I am now trying to create a dynamic route to fetch News or Events from my the repository.
class ContentController extends AbstractController {
/**
* #Route("/{type}", name="get_content")
*/
public function get(string $type) { //type is the name of the entity e.g. 'news' or 'event'
//fetch from repo
$results = $this->getDoctrine()->getRepository(/* entity class */)->findAll();
}
}
I already came up with some ideas, but not sure if they are good practise.
1) use full namespace for the entity (how do I check if the class exists? error handling?)
$results = $this->getDoctrine()->getRepository('App\\Entity\\News')->findAll();
$results = $this->getDoctrine()->getRepository('App\\Entity\\' . ucfirst($type))->findAll();
2) provide a route for each entity and call a generic function (not exactly what I want because I have like 10+ entities for this)
class ContentController extends AbstractController {
/**
* #Route("/news", name="get_news")
*/
public function get_news() {
$results = getContent(News::class);
}
/**
* #Route("/event", name="get_event")
*/
public function get_event() {
$results = getContent(Event::class);
}
public function getContent($class) {
return $this->getDoctrine()->getRepository($class)->findAll();
}
}
Mabye some of you have better ideas/improvements and can help me out a bit.

You can define available for fetching entities manually.
class ContentController extends AbstractController
{
/**
* #Route("/{type}", name="get_content")
*/
public function get(string $type)
{ //type is the name of the entity e.g. 'news' or 'event'
//fetch from repo
$results = $this->getDoctrine()->getRepository($this->getEntityClassFromType($type))->findAll();
}
private function getEntityClassFromType(string $type): string
{
//define your entities manually
foreach ([News::class, Event::class] as $class) {
$parts = explode('\\', $class);
$entity = array_pop($parts);
if ($type === lcfirst($entity)) {
return $class;
}
}
throw new NotFoundHttpException();
}
}
Or check your entities dynamically using $entityManager->getMetadataFactory()->hasMetadataFor($className);

Related

Symfony 3.4 Doctrine: Change referenced table on runtime

In my symfony application products are stored in the table products. There is an Entity Product and Repository ProductRepository for it.
Some products automatically get archived and stored in another table, called products_archived. I created an Entity ProductArchived and duplicated the ProductRepository file to the file ProductArchivedRepository.php.
The tables products and products_archived have exactly the same structure and fields.
My goal:
When in the code a product is identified as an archived product, I want to be able to apply a function from the ProductRepository and NOT having to refer to a separate ProductArchivedRepository. I want to avoid having to use duplicated code.
Example:
ProductRepository.php:
public function getProductDataById($productId)
{
$qb = $this->createQueryBuilder('p');
// ...
return $qb->getQuery()->getArrayResult();
}
ProductArchivedRepository.php:
public function getProductDataById($productId)
{
$qb = $this->createQueryBuilder('p');
// ...
return $qb->getQuery()->getArrayResult();
}
ProductService.php:
public function getProductDataById($productId)
{
$repoProduct = $this->productRepository;
$repoProductArchived = $this->container->productArchivedRepository;
if ($repoProduct->findOneBy(['id' => $productId]) instanceof Product) {
$repoP = $repoProduct;
} else if ($repoProductArchived->findOneBy(['id' => $productId]) instanceof ProductArchived) {
$repoP = $repoProductArchived;
} else {
throw new NotFoundHttpException(
'Product neither found in table product nor in table product_archived.'
);
}
$productData = $repoP->getProductDataById($productId);
return $productData;
}
How do I achieve that ProductArchivedRepository.php becomes redundant?
You can use an abstract class that regroup the duplicated method(s)
abstract class AbstractProductRepository extends EntityRepository
{
public function getProductDataById($productId)
{
$qb = $this->createQueryBuilder('p');
// ...
return $qb->getQuery()->getArrayResult();
}
}
And now, both of your repository can extend the abstract class instead of EntityRepository :
class ProductRepository extends AbstractProductRepository
{
// ...
}
class ProductArchivedRepository extends AbstractProductRepository
{
// ...
}

How can I make a specific function on action page to handle the form data?

I have a models.php page that contains the specification of form for a specific model.
models.php
$books = [
['Book Name', 'text' ],
['Author', 'text']
];
$vegetables = [
['Name', 'text'],
['Photo', 'file']
]
Now this page is accessed by an admin.php page, which generate an appropriate HTML form on the basis of the given name and input type.
I want to fill the form and send the data into a handle.php and handle the data with the specific function to fill the data into appropriate table.
handle.php
function books(){
// this will fill the details into table of books.
INSERT INTO BOOKS
name = $_POST['book_name']
author $_POST['author']
}
function vegetables(){
// this will fill the details into table of vegetables.
INSERT INTO VEGETABLES
name = $_POST['book_name']
photo = $_FILE['photo']
}
(If there's any other better way of doing this, so please mention, I'll do that way and delete my question.)
Here's my suggestion. As stated in the comments, this is just my way to do such things, it's not necessarily the best solution for every situation.
I have a base model, that defines all methods all model need to have in common. Here's a very simplified version:
class Model {
public $modelName = 'default';
public $id = null;
private $fields = [];
private $tableName = 'default';
private $tableDefinition = [];
private $idField = 'id';
public function insert($dataset) {
// do some database magic by using $this->fields, or $this->tableDefinition
$sql = "INSERT into {$this->tableName} ...";
...
return $id;
}
public function update($id, $dataset) {
// do some more database magic by using $this->fields, or $this->tableDefinition
}
// many more methods. To get data, delete, sort, ..
//...
}
Every model now extends this base model class and sets it's specific params, maybe even overrides some methods or adds special ones:
class Books extends Model {
public $modelName = 'book';
private $fields = ['bookName','Author'];
private $tableName = 'BOOKS';
private $tableDefinition = [
['bookName','varchar'],
['Author','varchar']
];
// private $idField = 'id'; // you can ommit that, if it's the default.
}
If Vegetables behaves different you can simply override a method:
class Vegetables extends Model {
public $modelName = 'vegetable';
// set all other properties...
// override insert() for example
public function insert($dataset) {
// do something that doesn't comply with the standard procedure
}
}
Then in handle.php you can do something like this:
<?php
$modelName = $request; // get it from your form, your url, ..
// & verify this model(file) exists.
$model = new $modelName();
$model->insert($dataSet);
Make a base interface BaseModel.php
which would have the basic signatures of insertion , updation and selection
Make a derived class booksModel.php and vegetablesModel.php that would implement the BaseModel class.
In this way, you have made your code extendable. If there is some common functionality, you can make the base class as Abstract class.
abstract class BaseModel {
abstract function add($dataObject);
abstract function get($dataObject);
}
class BooksModel extends BaseModel {
public function add($dataObject) {
/* Implementation */
}
public function get($dataObject) {
/* Implementation */
}
}
class VegetableModel extends BaseModel {
public function add($dataObject) {
/* Implementation */
}
public function get($dataObject) {
/* Implementation */
}
}

Laravel eloquent table-less data from external source

I'm developing an application where my data comes from external server in JSON format.
I would like to set a relationships between each models, but without using a database table.
Is it possible ?
Something like that:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'https://.../server/flights.json';
}
You could make a service class which handles the request and returns class instances:
namespace App\Services;
class FlightService
{
/**
* #var FlightFactory
*/
private $flightFactory;
public function __construct(FlightFactory $flightFactory)
{
$this->flightFactory = $flightFactory;
}
public function getAllFlights()
{
$flightsJson = $this->getFromExternalCurl();
return $this->flightFactory->buildFlightList($flightsJson);
}
private function getFromExternalCurl()
{
return Curl::to('http://www.foo.com/flights.json')
->withData( array( 'foz' => 'baz' ) )
->asJson()
->get();
}
}
Basically the service would make the external API call and the response is passed to a factory which creates the instances.
Note that you just need to add the factory in the construct and it's binded because laravel uses https://laravel.com/docs/5.4/container
namespace App\Factories;
class FlightFactory
{
public function buildFlightList($flightJsonList)
{
$flightCollection = collect();
foreach($flightJsonList as $flightJson) {
$flightCollection->push($this->buildFlight($flightJson));
}
return $flightCollection;
}
public function buildFlight($flightJson)
{
$flight = new Flight();
// add properties
return $flight;
}
}
The factory will return a Collection which is verry usefull because it contains usefull methods, or you can return an array.
In this example I used a curl library https://github.com/ixudra/curl but it can be replaced with native php or other libraries.
Then you can use by injecting the FlightService in your controllers.
P.S: Code not tested but represents a possible approach

Encryption/Decryption of Form Fields in CakePHP 3

I want to have some form-fields encrypted when they are added/edited and decrypted when they are looked up by cake.
Here is the code that works for me in v2.7.2:
core.php
Configure::write('Security.key','secretkey');
app/model/patient.php.
public $encryptedFields = array('patient_surname', 'patient_first_name');
public function beforeSave($options = array()) {
foreach($this->encryptedFields as $fieldName){
if(!empty($this->data[$this->alias][$fieldName])){
$this->data[$this->alias][$fieldName] = Security::encrypt(
$this->data[$this->alias][$fieldName],
Configure::read('Security.key')
);
}
}
return true;
}
public function afterFind($results, $primary = false) {
foreach ($results as $key => $val) {
foreach($this->encryptedFields as $fieldName) {
if (#is_array($results[$key][$this->alias])) {
$results[$key][$this->alias][$fieldName] = Security::decrypt(
$results[$key][$this->alias][$fieldName],
Configure::read('Security.key')
);
}
}
}
return $results;
}
As I understand it I have to replace $this->data[] with the generated entities for the model and the afterFind method with virtual fields, but I just can't put it all together.
There's more than one way to solve this (please note that the following code is untested example code! You should get a grasp on the new basics first before using any of this).
A custom database type
One would be a custom database type, which would encrypt when binding the values to the database statement, and decrypt when results are being fetched. That's the option that I would prefer.
Here's simple example, assuming the db columns can hold binary data.
src/Database/Type/CryptedType.php
This should be rather self explantory, encrypt when casting to database, decrypt when casting to PHP.
<?php
namespace App\Database\Type;
use Cake\Database\Driver;
use Cake\Database\Type;
use Cake\Utility\Security;
class CryptedType extends Type
{
public function toDatabase($value, Driver $driver)
{
return Security::encrypt($value, Security::getSalt());
}
public function toPHP($value, Driver $driver)
{
if ($value === null) {
return null;
}
return Security::decrypt($value, Security::getSalt());
}
}
src/config/bootstrap.php
Register the custom type.
use Cake\Database\Type;
Type::map('crypted', 'App\Database\Type\CryptedType');
src/Model/Table/PatientsTable.php
Finally map the cryptable columns to the registered type, and that's it, from now on everything's being handled automatically.
// ...
use Cake\Database\Schema\Table as Schema;
class PatientsTable extends Table
{
// ...
protected function _initializeSchema(Schema $table)
{
$table->setColumnType('patient_surname', 'crypted');
$table->setColumnType('patient_first_name', 'crypted');
return $table;
}
// ...
}
See Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
beforeSave and result formatters
A less dry and tighter coupled approach, and basically a port of your 2.x code, would be to use the beforeSave callback/event, and a result formatter. The result formatter could for example be attached in the beforeFind event/callback.
In beforeSave just set/get the values to/from the passed entity instance, you can utilize Entity::has(), Entity::get() and Entity::set(), or even use array access since entities implement ArrayAccess.
The result formatter is basically an after find hook, and you can use it to easily iterate over results, and modify them.
Here's a basic example, which shouldn't need much further explanation:
// ...
use Cake\Event\Event;
use Cake\ORM\Query;
class PatientsTable extends Table
{
// ...
public $encryptedFields = [
'patient_surname',
'patient_first_name'
];
public function beforeSave(Event $event, Entity $entity, \ArrayObject $options)
{
foreach($this->encryptedFields as $fieldName) {
if($entity->has($fieldName)) {
$entity->set(
$fieldName,
Security::encrypt($entity->get($fieldName), Security::getSalt())
);
}
}
return true;
}
public function beforeFind(Event $event, Query $query, \ArrayObject $options, boolean $primary)
{
$query->formatResults(
function ($results) {
/* #var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
return $results->map(function ($row) {
/* #var $row array|\Cake\DataSource\EntityInterface */
foreach($this->encryptedFields as $fieldName) {
if(isset($row[$fieldName])) {
$row[$fieldName] = Security::decrypt($row[$fieldName], Security::getSalt());
}
}
return $row;
});
}
);
}
// ...
}
To decouple this a little, you could also move this into a behavior so that you can easily share it across multiple models.
See also
Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields
Cookbook > Tutorials & Examples > Bookmarker Tutorial Part 2 > Persisting the Tag String
Cookbook > Database Access & ORM > Behaviors
API > \Cake\Datasource\EntityTrait
API > \Cake\ORM\Table
Edit: #npm was right about the virtual properties not working. Now i'm angry at myself for giving a bad answer. serves me right for not checking it before I posted.
To make it right, I've implemented a version using behaviors to decrypt the fields as they are read, and encrypt them as they are written to the database.
Note: This code does not currently incorporate any custom finders, so it will not support searching by the encrypted field.
eg.
$this->Patient->findByPatientFirstname('bob'); // this will not work
Behavior
/src/Model/Behavior/EncryptBehavior.php
<?php
/**
*
*/
namespace Cake\ORM\Behavior;
use ArrayObject;
use Cake\Collection\Collection;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\ResultSetInterface;
use Cake\Event\Event;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
use Cake\Utility\Security;
use Cake\Log\Log;
/**
* Encrypt Behavior
*/
class EncryptBehavior extends Behavior
{
/**
* Default config
*
* These are merged with user-provided configuration when the behavior is used.
*
* #var array
*/
protected $_defaultConfig = [
'key' => 'YOUR_KEY_KERE', /* set them in the EntityTable, not here */
'fields' => []
];
/**
* Before save listener.
* Transparently manages setting the lft and rght fields if the parent field is
* included in the parameters to be saved.
*
* #param \Cake\Event\Event $event The beforeSave event that was fired
* #param \Cake\ORM\Entity $entity the entity that is going to be saved
* #return void
* #throws \RuntimeException if the parent to set for the node is invalid
*/
public function beforeSave(Event $event, Entity $entity)
{
$isNew = $entity->isNew();
$config = $this->config();
$values = $entity->extract($config['fields'], true);
$fields = array_keys($values);
$securityKey = $config['key'];
foreach($fields as $field){
if( isset($values[$field]) && !empty($values[$field]) ){
$entity->set($field, Security::encrypt($values[$field], $securityKey));
}
}
}
/**
* Callback method that listens to the `beforeFind` event in the bound
* table. It modifies the passed query
*
* #param \Cake\Event\Event $event The beforeFind event that was fired.
* #param \Cake\ORM\Query $query Query
* #param \ArrayObject $options The options for the query
* #return void
*/
public function beforeFind(Event $event, Query $query, $options)
{
$query->formatResults(function ($results){
return $this->_rowMapper($results);
}, $query::PREPEND);
}
/**
* Modifies the results from a table find in order to merge the decrypted fields
* into the results.
*
* #param \Cake\Datasource\ResultSetInterface $results Results to map.
* #return \Cake\Collection\Collection
*/
protected function _rowMapper($results)
{
return $results->map(function ($row) {
if ($row === null) {
return $row;
}
$hydrated = !is_array($row);
$fields = $this->_config['fields'];
$key = $this->_config['key'];
foreach ($fields as $field) {
$row[$field] = Security::decrypt($row[$field], $key);
}
if ($hydrated) {
$row->clean();
}
return $row;
});
}
}
Table
/src/Model/Table/PatientsTable.php
<?php
namespace App\Model\Table;
use App\Model\Entity\Patient;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\Core\Configure;
/**
* Patients Model
*
*/
class PatientsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('patients');
$this->displayField('id');
$this->primaryKey('id');
// will encrypt these fields automatically
$this->addBehavior('Encrypt',[
'key' => Configure::read('Security.key'),
'fields' => [
'patient_surname',
'patient_firstname'
]
]);
}
}
I feel your pain. the ORM layer in cakephp 3 is radically different from cake2. They split the entity model and the table ORM into two different classes, and afterFind has been removed. I would take a look at using virtual properties. I think it might be suitable for your use case.
Example below.
<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\Utility\Security;
use Cake\Core\Configure;
class Patient extends Entity
{
protected function _setPatientSurname($str)
{
$this->set('patient_surname', Security::encrypt($str, Configure::read('Security.key'));
}
protected function _setPatientFirstname($str)
{
$this->set('patient_firstname', Security::encrypt($str, Configure::read('Security.key'));
}
protected function _getPatientSurname()
{
return Security::decrypt($this->patient_surname, Configure::read('Security.key'));
}
protected function _getPatientFirstname()
{
return Security::decrypt($this->patient_first_name, Configure::read('Security.key'));
}
}

Laravel Model Events - I'm a bit confused about where they're meant to go

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!

Categories