Easy admin refuses entity update unless I edit image field - php

I am fixing an existing Symfony 5.4 website running on PHP 8.0.18. The back office is handled by EasyAdmin 3.4.
I can't figure out what's wrong. Like the title says, when I go to edit an "Event" entity, the save buttons won't work at all unless I re-upload a different event picture. No amount of editing the other fields will work, and I can use the save buttons on other entities even if I have made no modification to the entity. I've looked though my configuration and entity setup but so far, I don't get it.
Edit: other entities with ImageField also refuse to be updated unless I've re-uploaded something. I found a temporaty fix by using setRequired(false) in the event crud conf, but an image is definitely required in this case, so I'm just setting myself up for a different kind of failure if I'm not mistaken. Is this really the only way?
Event entity:
<?php
namespace App\Entity;
use App\Repository\EventRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=EventRepository::class)
*/
class Event
{
// ...
/**
* #ORM\Column(type="string", length=255)
*/
private $src;
// ...
public function getSrc(): ?string
{
return $this->src;
}
public function setSrc(string $src): self
{
$this->src = $src;
return $this;
}
// ...
}
Event crud controller:
<?php
namespace App\Controller\Admin;
use App\Entity\Event;
use App\Entity\TranslationString;
use App\Entity\TranslationText;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
class EventCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Event::class;
}
public function configureCrud(Crud $crud): Crud
{
return $crud
->setSearchFields([
'date',
'end',
'title.fr',
'title.en',
'body.fr',
'body.en',
'alt.fr',
'alt.en',
])
->setDefaultSort(['archived' => 'ASC','date' => 'DESC',]);
}
public function configureFields(string $pageName): iterable
{
return [
DateField::new('date'),
DateField::new('end'),
TextField::new('titleFr'),
TextField::new('titleEn')->hideOnIndex(),
BooleanField::new('isShow'),
BooleanField::new('archived'),
TextareaField::new('bodyFr'),
TextareaField::new('bodyEn')->hideOnIndex(),
ImageField::new('src')
->setBasePath('img/events')
->setUploadDir('www/img/events'),
TextareaField::new('altFr')->hideOnIndex(),
TextareaField::new('altEn')->hideOnIndex(),
];
}
public function createEntity(string $Fqcn): Event
{
return (new Event)
->setAlt(new TranslationText)
->setTitle(new TranslationString)
->setBody(new TranslationText);
}
}

I had the same problem and I think that the following code will help
public function configureFields(string $pageName): iterable {
$imageField = ImageField::new('image', 'Image')->setUploadDir('public/uploads/images/')->setBasePath('uploads/images/');
if ($pageName != 'new') {
$imageField->setRequired(false);
}
return [
TextField::new('title'),
$imageField,
TextEditorField::new('description')->setNumOfRows(20),
UrlField::new('ticketOfficeLink'),
AssociationField::new('eventStates')
];
}

Related

Doctrine SQLFilter with QueryBuilder in Symfony not working

I'm trying to create an SQLFilter for a query in my Symfony app.
The issue is that the filter is not applied on the query (and not called), even though is it enabled correctly (see below).
The repository is not linked to an entity, because the database is external to my app, but it still has access to the data.
Am I missing something ?
Here's the filter:
<?php
namespace App\SQL\Filter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class UserRoleFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
return 'c.roleId = 1';
}
}
I registered it in config/packages/doctrine.yaml:
doctrine:
filters:
user_role: App\SQL\Filter\UserRoleFilter
The controller:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Doctrine\Persistence\ManagerRegistry;
use App\Repository\CustomerRepository;
class CustomerController extends AbstractController
{
public function myAction(Request $request, ManagerRegistry $doctrine, CustomerRepository $customerRepository)
{
$doctrine->getManager()->getFilters()->enable('user_role');
$customers = $customerRepository->findAll();
}
}
The repository:
<?php
namespace App\Repository;
use Doctrine\DBAL\Connection;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
class CustomerRepository
{
protected Connection $conn;
protected ObjectManager $em;
public function __construct(ManagerRegistry $doctrine)
{
$this->em = $doctrine->getManager();
$this->conn = $this->em->getConnection();
}
public function findAll(): array
{
dump($this->em->getFilters()->isEnabled('user_role')); // returns true
return $this->conn->createQueryBuilder()
->select('c.*')
->from('customer', 'c')
->executeQuery()
->fetchAllAssociative();
}
}
From looking at the source for Doctrine/DBAL, it doesn't look like the filter would ever be applied to the query you are executing.
Within your repository class you are creating an instance of Doctrine\DBAL\Query\QueryBuilder which only holds a reference to Doctrine\DBAL\Connection.
Then the select data is set to its private parameter $sqlParts.
When executeQuery() is called is concatenates the content of sqlParts into a string with no mention or reference to any filter objects nor their constraints. This can be seen on line 308 of the QueryBuilder class.
QueryBuilder::executeQuery()
You can also see how the select query is concatenated on line 1320 of QueryBuilder.
QueryBuilder::getSQLForSelect()
The only way I can see to add it easily would be to add it directly to a where clause, e.g.
public function findAll(): array
{
return $this->conn->createQueryBuilder()
->select('c.*')
->from('customer', 'c')
->where("c.roleId = 1") // Or pull it from the filter object in some way
->executeQuery()
->fetchAllAssociative();
}
If you want to see where the filter constraints are added to the queries you can find that data in the ORM package from Doctrine, however these are all linked to entities and table aliases.
SqlWalker::generateFilterConditionSQL()
BasicEntityPersistor::generateFilterConditionSQL()
ManyToManyPersistor::generateFilterConditionSQL()

New alternative for getDoctrine() in Symfony 5.4 and up

As my IDE points out, the AbstractController::getDoctrine() method is now deprecated.
I haven't found any reference for this deprecation neither in the official documentation nor in the Github changelog.
What is the new alternative or workaround for this shortcut?
As mentioned here:
Instead of using those shortcuts, inject the related services in the constructor or the controller methods.
You need to use dependency injection.
For a given controller, simply inject ManagerRegistry on the controller's constructor.
use Doctrine\Persistence\ManagerRegistry;
class SomeController {
public function __construct(private ManagerRegistry $doctrine) {}
public function someAction(Request $request) {
// access Doctrine
$this->doctrine;
}
}
You can use EntityManagerInterface $entityManager:
public function delete(Request $request, Test $test, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$test->getId(), $request->request->get('_token'))) {
$entityManager->remove($test);
$entityManager->flush();
}
return $this->redirectToRoute('test_index', [], Response::HTTP_SEE_OTHER);
}
As per the answer of #yivi and as mentionned in the documentation, you can also follow the example below by injecting Doctrine\Persistence\ManagerRegistry directly in the method you want:
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* #Route("/product", name="create_product")
*/
public function createProduct(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Add code in controller, and not change logic the controller
<?php
//...
use Doctrine\Persistence\ManagerRegistry;
//...
class AlsoController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
'doctrine' => '?'.ManagerRegistry::class,
]);
}
protected function getDoctrine(): ManagerRegistry
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
...
}
read more https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
In my case, relying on constructor- or method-based autowiring is not flexible enough.
I have a trait used by a number of Controllers that define their own autowiring. The trait provides a method that fetches some numbers from the database. I didn't want to tightly couple the trait's functionality with the controller's autowiring setup.
I created yet another trait that I can include anywhere I need to get access to Doctrine. The bonus part? It's still a legit autowiring approach:
<?php
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Symfony\Contracts\Service\Attribute\Required;
trait EntityManagerTrait
{
protected readonly ManagerRegistry $managerRegistry;
#[Required]
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
// #phpstan-ignore-next-line PHPStan complains that the readonly property is assigned outside of the constructor.
$this->managerRegistry = $managerRegistry;
}
protected function getDoctrine(?string $name = null, ?string $forClass = null): ObjectManager
{
if ($forClass) {
return $this->managerRegistry->getManagerForClass($forClass);
}
return $this->managerRegistry->getManager($name);
}
}
and then
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\Foobar;
class SomeController extends AbstractController
{
use EntityManagerTrait
public function someAction()
{
$result = $this->getDoctrine()->getRepository(Foobar::class)->doSomething();
// ...
}
}
If you have multiple managers like I do, you can use the getDoctrine() arguments to fetch the right one too.

Symfony 4, find() an entity

Thanks to the symfony doc https://symfony.com/doc/current/doctrine.html#updating-an-object, i am able to use get/set methods from my entity called 'Produit'.
But when i call the method setProduit() (from Paniers entity) Phpstorm say "expected App\Entity\Paniers, got Object" for the $produitSelected.
i don't know why phpstorm say that because i'm able to use methods, what's the problem ?
find() return an entity object, right ?
class PaniersController extends AbstractController
{
/**
* #Route("/paniers/add/{id}", name="paniers.add")
*/
public function addPanier(Request $request, Environment $twig, RegistryInterface $doctrine, $id)
{
$produitSelected = $doctrine->getRepository(Produit::class)->find($id);
if (!$produitSelected) {
throw $this->createNotFoundException(
'Pas de produit trouvé pour l\' id : '.$id
);
}
$lignePanier=$doctrine->getRepository(Paniers::class)->findOneBy(['produit' => $produitSelected, 'user' =>$this->getUser()]);
if($produitSelected->getStock()>0){
if ($lignePanier){
$quantite =$lignePanier->getQuantite();
$lignePanier->setQuantite($quantite+1);
} else {
$lignePanier = new Paniers();
$lignePanier->setUser($this->getUser())
->setDateAjoutPanier(new \DateTime(date('Y-m-d')))
->setQuantite(1)
->setProduit($produitSelected);
}
$doctrine->getManager()->persist($lignePanier);
$produitSelected->setStock($produitSelected->getStock()-1);
$doctrine->getManager()->persist($produitSelected);
$doctrine->getManager()->flush();
}
}
}
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\PaniersRepository")
* #ORM\Table(name="paniers")
*/
class Paniers
{
//...
/**
* #ORM\ManyToOne(targetEntity="Produit")
* #ORM\JoinColumn(name="produit_id", referencedColumnName="id")
*/
private $produit;
public function getProduit(): ?Produit
{
return $this->produit;
}
public function setProduit(?Produit $produit): self
{
$this->produit = $produit;
return $this;
}
}
In addition to Denis V's correct answer, I want to add that you could also modify your controller like this:
public function addPanier(Request $request, Environment $twig, RegistryInterface $doctrine,ProduitRepository $produitRepository, $id)
{
$produitSelected = $produitRepostiory->find($id);
//rest of the code
}
This way phpstorm also knows the type of the returned object, as every returned object is type hinted in the corresponding repository.
PhpStorm is apparently not that smart to understand that the actual type of the return value of find($id) in this case will be Produit. But you can help it:
/** #var Produit $produitSelected */
$produitSelected = $doctrine->getRepository(Produit::class)->find($id);
To make it working you should either use Produit with the full namespace or add the namespace directly within the type hint.
Of course this doesn't guarantee or ensure that the actual type will be Produit. So, if you make a mistake, PhpStorm will report the type incorrectly.
You first need to fix code :
Don't inject $id, you only need to typehint your entity: https://symfony.com/doc/current/best_practices/controllers.html#using-the-paramconverter
Don't inject $doctrine, use $em = $this->getDoctrine()->getManager();
Don't inject $twig, use return $this->render('...template', []);
Use English, it is always the rule. Not only other people could help you but also because Symfony understands it and you will need that when you start using form collections: https://symfony.com/doc/current/form/form_collections.html
Inject repository and you will have autocomplete and spot errors easier. Use ```make:entity`` command and you will see what I think, hard to explain.

Zend Framework 2 - Showing the content of a database

I'm making some kind of market site with Zend Framework 2. The home got a slider showing all the products (realized with CSS3 keyframes) and some text. Both the sliding pictures and the text are read from a MySQL database. But as result, i get no output but also no errors. The slider gets as many pictures as database rows, but still no content is echoed; plus if i try to change things (like db credentials or getter functions in model) it throws errors as expected, so it clearly reads the db and the problem is elsewhere.
Db for text has 3 fields:
id
name
text
Model for text (Home.php; there's an HomeInterface.php defining all the functions)
<?php
namespace Site\Model;
class Home implements HomeInterface {
protected $id;
protected $name;
protected $text;
public function getId() {
return $this->id;
}
public function getName() {
return $this->name;
}
public function getText() {
return $this->text;
}
}
?>
Mapper for text
<?php
namespace Site\Mapper;
use Site\Model\HomeInterface;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Adapter\Driver\ResultInterface;
use Zend\Stdlib\Hydrator\HydratorInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\Sql\Sql;
use Zend\Stdlib\Hydrator\ClassMethods;
class TextMapper implements TextMapperInterface {
protected $homePrototype;
protected $adapter;
protected $hydrator;
public function __construct(AdapterInterface $adapter, HomeInterface $homePrototype, HydratorInterface $hydrator) {
$this->adapter = $adapter;
$this->homePrototype = $homePrototype;
$this->hydrator = $hydrator;
}
public function find($name) {
$sql = new Sql($this->adapter);
$select = $sql->select();
$select->from("mono");
$select->where(array("name = ?" => $name));
$stmt = $sql->prepareStatementForSqlObject($select);
$result = $stmt->execute();
if ($result instanceof ResultInterface && $result->isQueryResult() && $result->getAffectedRows()) {
return $this->hydrator->hydrate($result->current(), $this->homePrototype);
}
throw new \InvalidArgumentException("{$name} non esiste.");
}
}
?>
Mapper for text has a factory, since it has dependencies:
<?php
namespace Site\Factory;
use Site\Mapper\TextMapper;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Site\Model\Home;
use Zend\Stdlib\Hydrator\ClassMethods;
class TextMapperFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
return new TextMapper($serviceLocator->get("Zend\Db\Adapter\Adapter"), new Home(), new ClassMethods(false));
}
}
?>
Service for text:
<?php
namespace Site\Service;
use Site\Model\Home;
use Site\Model\HomeInterface;
use Site\Mapper\TextMapperInterface;
class HomeService implements HomeServiceInterface {
protected $textMapper;
public function __construct (TextMapperInterface $textMapper) {
$this->textMapper = $textMapper;
}
public function findText($name) {
return $this->textMapper->find($name);
}
}
?>
Factory for this service:
<?php
namespace Site\Factory;
use Site\Service\HomeService;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class HomeServiceFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
$textMapper = $serviceLocator->get("Site\Mapper\TextMapperInterface");
return new HomeService($textMapper);
}
}
?>
Controller
<?php
namespace Site\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Site\Service\HomeServiceInterface;
use Zend\View\Model\ViewModel;
class SkeletonController extends AbstractActionController {
protected $homeService;
public function __construct(HomeServiceInterface $homeService) {
$this->homeService = $homeService;
}
public function indexAction() {
return new ViewModel(array (
"home" => $this->homeService->findText("home")
));
}
}
?>
Finally, the view:
<?php echo $this->home->getText(); ?>
Code for slider is similar and both the parts of this page are likely having the same problem. As i said, db is detected, tables and columns too, they aren't empty but nothing gets echoed. Interfaces are properly written, defining all the functions. All views are in the Site\view\Site\Skeleton folder. Any clues about where the problem is? Thank you.
Your code looks good. The only issue I can see is that you are using the ClassMethods hydrator and you have no setters on your entity.
The hydrator will use the entity API to hydrate the entity, if the setId, setName or setText are not callable then the values will not be set.
Although I recommend adding the missing methods you can also use the Zend\Stdlib\Hydrator\Reflection to set the entity properties without setters (via SPL ReflectionProperty)

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