Symfony 2 load different template depending on user agent properties - php

Is it possible (and how) to
determine if a user is using a mobile device
force symfony 2 to load different template in that case
(and fall back the default html template)
What id like to do is, to load different templates without modifying any controller.
UPDATE
It wasn't the detection part the real issue here, it's really nothing to do with symfony. It can be done (load different template) on a controller level:
public function indexAction()
{
$format = $this->isMobile() ? 'mob' : 'html';
return $this->render('AcmeBlogBundle:Blog:index.'.$format.'.twig');
}
But can it be done globally? Like a service, or something that execute before every request, and make changes in the templating rules.

Ok, so I don't have a full solution but a little more than where to look for one :)
You can specify loaders (services) for templating item in app/config/config.yml
framework:
esi: { enabled: true }
#translator: { fallback: %locale% }
secret: %secret%
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: %kernel.debug%
form: true
csrf_protection: true
validation: { enable_annotations: true }
templating:
engines:
- twig
loaders: [moby.loader]
default_locale: %locale%
trust_proxy_headers: false
session: ~
Then define the mentioned loader service:
services:
moby.loader:
class: Acme\AppBundle\Twig\Loader\MobyFilesystemLoader
arguments: ["#templating.locator", "#service_container"]
After that define your loader service class:
namespace Acme\AppBundle\Twig\Loader;
use Symfony\Bundle\FrameworkBundle\Templating\Loader\FilesystemLoader;
use Symfony\Component\Templating\Storage\FileStorage;
class MobyFilesystemLoader extends FilesystemLoader
{
protected $container;
public function __construct($templatePathPatterns, $container)
{
parent::__construct($templatePathPatterns);
$this->container = $container;
}
public function load(\Symfony\Component\Templating\TemplateReferenceInterface $template)
{
// Here you can filter what you actually want to change from html
// to mob format
// ->get('controller') returns the name of a controller
// ->get('name') returns the name of the template
if($template->get('bundle') == 'AcmeAppBundle')
{
$request = $this->container->get('request');
$format = $this->isMobile($request) ? 'mob' : 'html';
$template->set('format', $format);
}
try {
$file = $this->locator->locate($template);
} catch (\InvalidArgumentException $e) {
return false;
}
return new FileStorage($file);
}
/**
* Implement your check to see if request is made from mobile platform
*/
private function isMobile($request)
{
return true;
}
}
As you can see this isn't the full solution, but I hope that this, at least, points you to the right direction.
EDIT: Just found out that there is a bundle with mobile detection capabilities, with custom twig engine that renders template file depending on a device that sent request
ZenstruckMobileBundle, although I never used it so... :)

Well, you can use LiipThemeBundle.

You can utilize kernel.view event listener. This event comes to action when controller returns no response, only data. You can set reponse according to user agent property. For example
In your controller,
public function indexAction()
{
$data = ... //data prepared for view
$data['template_name'] = "AcmeBlogBundle:Blog:index";
return $data;
}
And the in your kernel.view event listener,
<?php
namespace Your\Namespace;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Templating\EngineInterface;
Class ViewListener
{
/**
* #var EngineInterface
*/
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$data = $event->getControllerResult(); //result returned by the controller
$templateName = $data['template_name'];
$format = $this->isMobile() ? 'mob' : 'html'; //isMobile() method may come from a injected service
$response = $this->templating->renderResponse($templateName . "." . $format . "twig", $data);
$event->setResponse($response);
}
}
Service definition,
your_view_listener.listener:
class: FQCN\Of\Listener\Class
arguments: [#templating]
tags:
- { name: kernel.event_listener, event: kernel.view, method: onKernelView }

This is what did the trick for me in Symfony 2.0:
Override twig.loader service so we can set our custom class:
twig.loader:
class: Acme\AppBundle\TwigLoader\MobileFilesystemLoader
arguments:
locator: "#templating.locator"
parser: "#templating.name_parser"
And create our custom class, that just sets "mob" format to the templates in case the client is a mobile device:
namespace Acme\AppBundle\TwigLoader;
use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader;
class MobileFilesystemLoader extends FilesystemLoader
{
public function findTemplate($template)
{
if ($this->isMobile()) {
$template->set('format', 'mob');
}
return parent::findTemplate($template);
}
private function isMobile()
{
//do whatever to detect it
}
}

I would suggest that this is not best handled by the controller but by CSS media queries, and serving a separate stylesheet to different classes of devices based on the results of that CSS media query.
A good intro here:
http://www.adobe.com/devnet/dreamweaver/articles/introducing-media-queries.html
and I would try reading http://www.abookapart.com/products/responsive-web-design in great detail. Some thinking has been done since the book was published, but it will get you headed the right direction.

From my experiences, you can but by specifying a format in the first place - check these docs, they may be able to assist you

I think is nothing to do with symfony. Templates are for the VIEW. You may achieve this by using different CSS for the same template to get different layout (template). I am using jQuery and CSS to handle different devices. You may want to look at some source code of the UI from http://themeforest.net/; specifically this template. This is one handles different device.

Alternative: https://github.com/suncat2000/MobileDetectBundle
I found it quite good compared to https://github.com/kbond/ZenstruckMobileBundle and https://github.com/liip/LiipThemeBundle

Related

Symfony 6 - Creating excel with PHPSpreadsheet and downloading it asynchronously

I'm stuck on something, and it seems internet haven't had this problem (or i haven't got the right keyword to find the answer)
Keep in mind that I'm still learning Symfony 6 and I'm a bit by myself for now ... So I'm open if you tell me that everything I did is garbage.
I'm creating an application to export datas in excels for our customers.
I create a call on a database, with a specific SQL request to get my datas. Then i send the datas in a SpreadsheetService to create my spreadsheet and launch the download for the user.
<?php
namespace App\Service;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\StreamedResponse;
class SpreadsheetService {
public function export(string $title, $datas, $rsm) {
$streamedResponse = new StreamedResponse();
$streamedResponse->setCallback(function () use ($title, $datas, $rsm) {
// Generating SpreadSheet
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle($title);
// Generating First Row with column name
$sheet->fromArray($rsm->scalarMappings);
// Generating other rows with datas
$count = 2;
foreach ($datas as $data) {
$sheet->fromArray($data, null, 'A' . $count);
$count++;
}
// Write and send created spreadsheet
$writer = new Xlsx($spreadsheet);
$writer->save('php://output');
// This exit(); is required to prevent errors while opening the generated .xlsx
exit();
});
// Puting headers on response and sending it
$streamedResponse->setStatusCode(Response::HTTP_OK);
$streamedResponse->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$streamedResponse->headers->set('Content-Disposition', 'attachment; filename="' . $title . '.xlsx"');
$streamedResponse->send();
return;
}
So, this is working like a charm. BUT, my chief want it to be asynchronous.
After some research on Symfony 6 and async in Symfony, I happened to find something called symfony/messenger, which at first sounded like it was only for send messages (mail, chat, sms ...) but after some reading, sounded like the async library for Symfony 6.
So, i tried by following step by step to setup the async.
First, i created an ExportMessage.php
<?php
namespace App\Message;
class ExportMessage {
// I need a slug to know for which customer i want the export
private string $slug;
// I need this string to know if it is an export for their clients, or their candidats etc ... (Wanted to setup an enum, but failed trying for now ...)
private string $typeExport;
public function __construct(string $slug, string $typeExport) {
$this->slug = $slug;
$this->typeExport = $typeExport;
}
/**
* Get the value of slug
*/
public function getSlug() {
return $this->slug;
}
/**
* Get the value of typeExport
*/
public function getTypeExport() {
return $this->typeExport;
}
}
Then i created an ExportHandler.php that will do some work when i send an ExportMessage (They go together)
<?php
namespace App\MessageHandler;
use App\Message\ExportMessage;
use App\Service\ClientRequestService;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class ExportHandler implements MessageHandlerInterface {
private ClientRequestService $clientRequestService;
// clientRequestService is the service that send the sql request and then call the SpreadsheetService to create the excel file
public function __construct(ClientRequestService $clientRequestService) {
$this->clientRequestService = $clientRequestService;
}
public function __invoke(ExportMessage $message) {
return $this->clientRequestService->export($message->getSlug(), $message->getTypeExport());
}
}
Finally, in my Controller, I don't call clientRequestService->export anymore, i create a messageBus that will keep track of any messages i send, and will process them correctly (Something like that, I didn't understand every aspect of it for now I think)
class DashboardController extends AbstractController {
private MessageBusInterface $messageBus;
public function __construct([...], MessageBusInterface $messageBus) {
[...]
$this->messageBus = $messageBus;
}
[...]
#[Route('{slug}/export-candidats', name: 'app_export_candidats')]
public function exportCandidats(string $slug) {
$this->messageBus->dispatch(new ExportMessage($slug, 'candidats'));
// Not anymore --> $this->requestService->export($slug, 'candidats');
return $this->redirectToRoute('app_dashboard', ['slug' => $slug]);
}
[...]
And just for the sake of it, here's the clientRequestService.php in case
<?php
namespace App\Service;
use App\Service\MappingService;
use App\Service\SpreadsheetService;
use App\Factory\EntityManagerFactory;
use App\Repository\Istrator\DatabaseGroupRepository;
class ClientRequestService {
private $factory;
private $databaseGroupRepository;
private $mappingService;
private $spreadsheetService;
private $rootpath_sql_request;
public function __construct(EntityManagerFactory $factory, DatabaseGroupRepository $databaseGroupRepository, MappingService $mappingService, SpreadsheetService $spreadsheetService, string $rootpath_sql_request) {
$this->factory = $factory;
$this->databaseGroupRepository = $databaseGroupRepository;
$this->mappingService = $mappingService;
$this->spreadsheetService = $spreadsheetService;
$this->rootpath_sql_request = $rootpath_sql_request;
}
public function export(string $slug, $export) {
$databaseGroup = $this->databaseGroupRepository->findBySlug($slug);
$entityManager = $this->factory->createManager($databaseGroup->getIdDb());
switch ($export) {
case 'candidats':
$rsm = $this->mappingService->getMappingExportCandidats($entityManager);
$query = file_get_contents($this->rootpath_sql_request . "export_candidats.sql");
break;
case 'clients':
$rsm = $this->mappingService->getMappingExportClients($entityManager);
$query = file_get_contents($this->rootpath_sql_request . "export_clients.sql");
break;
case 'pieces_jointes':
$rsm = $this->mappingService->getMappingPiecesJointes($entityManager);
$query = file_get_contents($this->rootpath_sql_request . "export_noms_pj.sql");
break;
case 'notes_suivi':
$rsm = $this->mappingService->getMappingNotesSuivi($entityManager);
$query = file_get_contents($this->rootpath_sql_request . "export_notes_suivi.sql");
break;
default:
return;
}
$results = $entityManager->createNativeQuery($query, $rsm)->execute();
$this->spreadsheetService->export($export, $results, $rsm);
}
}
It seems to be okay, but this doesn't trigger the download ...
Can someone help me understand this problem ?
EDIT 1:
After some research, i found out that the Handler isn't even called.
I tried some thing :
In the messenger.yaml i defined my ExportMessage as async :
framework:
messenger:
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
retry_strategy:
max_retries: 3
multiplier: 2
failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
routing:
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Symfony\Component\Notifier\Message\ChatMessage: async
Symfony\Component\Notifier\Message\SmsMessage: async
// --------------- Here ----------------
App\Message\ExportMessage: async
And then in my services.yaml I defined my handler as a Service
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
App\MessageHandler\ExportHandler:
tags: [messenger.message_handler]
[...]
Maybe this can narrow down some problems. My handlers isn't called and i don't understand why.
My message is sent (It's created in the database)
| 30 | O:36:\"Symfony\\Component\\Messenger\\Envelope\":2:{s:44:\"\0Symfony\\Component\\Messenger\\Envelope\0stamps\";a:1:{s:46:\"Symfony\\Component\\Messenger\\Stamp\\BusNameStamp\";a:1:{i:0;O:46:\"Symfony\\Component\\Messenger\\Stamp\\BusNameStamp\":1:{s:55:\"\0Symfony\\Component\\Messenger\\Stamp\\BusNameStamp\0busName\";s:21:\"messenger.bus.default\";}}}s:45:\"\0Symfony\\Component\\Messenger\\Envelope\0message\";O:25:\"App\\Message\\ExportMessage\":2:{s:31:\"\0App\\Message\\ExportMessage\0slug\";s:4:\"toma\";s:37:\"\0App\\Message\\ExportMessage\0typeExport\";s:9:\"candidats\";}} | [] | default | 2022-04-26 14:36:53 | 2022-04-26 14:36:53 | NULL |
I continue to work on it.
EDIT 2 :
Ok, so I didn't understand how asynchrones buses worked in php, because I'm from Typescript, and asynchronous process in Typescript are really different compared to this.
I needed a consumers that will listen when there is a message pushed in the bus, and consume it, and send it to the handler...
The documentation explained that, but i didn't understand :
https://symfony.com/doc/current/the-fast-track/en/18-async.html#running-workers-in-the-background
So now, I can generate my excel file asynchronously. I just have to create something to watch for it to be created, and give a link to download it.
Hope this thread can help some people who, like me, didn't quite understand the bus mecanic.

Rendered view is not displaying in my back office in PS 1.7

I am trying to create a view in the back office tab that I created in the installation of my Module. My Module adds the tab like this:
protected function _installTabs()
{
if(!$tabId = \Tab::getIdFromClassName('IezonPortfolio')) {
$tab = new \Tab();
$tab->class_name = 'IezonPortfolio';
$tab->module = $this->name;
$tab->id_parent = \Tab::getIdFromClassName('ShopParameters');
$tab->active = 1;
foreach (Language::getLanguages(false) as $lang):
$tab->name[(int) $lang['id_lang']] = 'My Portfolio';
endforeach;
return $tab->save();
}
new \Tab((int) $tabId);
return true;
}
This works fine and I can navigate to my Shop Parameters and click the My Portfolio tab. The issue I'm having is that it is blank. My ModuleAdminController looks like this:
class IezonPortfolioController extends ModuleAdminController {
private $_module;
public function __construct()
{
$this->bootstrap = true;
parent::__construct();
$this->_module = \Module::getInstanceByName('iezonportfolio');
}
public function indexAction()
{
return $this->render('#Modules/iezonportfolio/views/templates/admin/display.html.twig', array(
'contents_iezonportfolio' => $this->_module->selectAll()
));
}
}
My display.html.twig just has test in it to see if it would output anything which it didn't. On looking at the Docs it doesn't mention anything other than using the render function and returning it. Any help would be appreciated. I just get a blank Tab.
EDIT: After looking at some of the pre-installed modules and referencing them to the Docs, I saw that I was missing my route configuration. My Controller is in the documented directory set-up: iezonportfolio/controllers/admin/iezonportfolio.php so I made my route like this:
iezonportfolio:
path: iezonportfolio
methods: [GET]
defaults:
_controller: 'IezonPortfolio\Controllers\Admin\Controller::indexAction'
_legacy_controller: 'IezonPortfolioController'
_legacy_link: 'IezonPortfolioController:index'
This has still not yet fixed the blank display so I tried to dig deeper into some other modules and have now updated my display.html.twig to show this:
{% extends '#PrestaShop/Admin/layout.html.twig' %}
{% block content %}
Test
{% endblock %}
This did not fix the blank display either. I hope this addition is useful for future viewers.
This is not how the modern controllers works, you are extending legacy ModuleAdminController, take a look here:
https://github.com/PrestaShop/example-modules
you have a plenty of module examples, here's a little snippet from one of those modules:
declare(strict_types=1);
namespace PrestaShop\Module\DemoControllerTabs\Controller\Admin;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use Symfony\Component\HttpFoundation\Response;
class MinimalistController extends FrameworkBundleAdminController
{
/**
* #return Response
*/
public function indexAction()
{
return $this->render('#Modules/democontrollertabs/views/templates/admin/minimalist.html.twig');
}
}
I recommend you to think wether you want to use, or not, modern controller. It depends on wether you want to sell your module, use it in client projects etc.

Accessing behat.yml variables within FeaturesContext

I just want to access the screen_shots_path parameter from FeatureContext.php file but writing $this->getMinkParameter('screen_shots_path'); doesn't work?
Anyone know how to do it?
Thanks in advance
I checked this one but the class extends BehatContext and mine extends MinkContext so I giot confused how to apply it mine.
sport/behat.yml
default:
context:
class: 'FeatureContext'
extensions:
Behat\Symfony2Extension\Extension:
mink_driver: true
kernel:
env: test
debug: true
Behat\MinkExtension\Extension:
base_url: 'http://localhost/local/sport/web/app_test.php/'
files_path: 'dummy/'
screen_shots_path: 'build/behat/'
browser_name: 'chrome'
goutte: ~
selenium2: ~
paths:
features: 'src/Football/TeamBundle/Features'
bootstrap: %behat.paths.features%/Context
sport/src/Football/TeamBundle/Features/Context/FeatureContext.php
namespace Football\TeamBundle\Features\Context;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Mink\Exception\UnsupportedDriverActionException;
use Behat\Mink\Driver\Selenium2Driver;
class FeatureContext extends MinkContext
{
/**
* Take screen-shot when step fails.
* Works only with Selenium2Driver.
*
* #AfterStep
* #param $event
* #throws \Behat\Mink\Exception\UnsupportedDriverActionException
*/
public function takeScreenshotAfterFailedStep($event)
{
if (4 === $event->getResult()) {
$driver = $this->getSession()->getDriver();
if (! ($driver instanceof Selenium2Driver)) {
throw new UnsupportedDriverActionException(
'Taking screen-shots is not supported by %s, use Selenium2Driver instead.',
$driver
);
return;
}
#$directory = 'build/behat';
$directory = $this->getMinkParameter('screen_shots_path');
if (! is_dir($directory)) {
mkdir($directory, 0777, true);
}
$filename = sprintf(
'%s_%s_%s.%s',
$this->getMinkParameter('browser_name'),
date('Y-m-d') . '_' . date('H:i:s'),
uniqid('', true),
'png'
);
file_put_contents($directory . '/' . $filename, $driver->getScreenshot());
}
}
}
I know you've tagged it as a Symfony question, there might be something on that side that affects it, but from the code it doesn't seem to be, so the problem is probably in the following.
Assuming you are using Mink Extension 1.x and not 2.x, screen_shots_path parameter is not on the list of the supported ones. In fact 2.x doesn't support it either, but it would throw an exception right away when it finds something illegal in the config. Perhaps 1.x doesn't do that. You can see the supported parameters here.
The most likely reason, screen_shots_path simply gets ignored when the config is normalised and hence getMinkParameter('screen_shots_path') doesn't return anything. I bet if you try the same with files_path you'll see dummy/.
If you want to keep the configuration in your behat.yml your best chances would be to pass them directly to the context, see documentation.
# behat.yml
default:
context:
class: FeatureContext
parameters:
screen_shots_path: 'build/behat/'
This will be passed to the constructor where you can initialise a local parameter. Alternatively you can use the static parameter and make it accessible through other contexts.
class FeatureContext extends MinkContext
{
protected $screenShotsPath;
public function __construct($parameters)
{
$this->screenShotsPath = isset($parameters['screen_shots_path']) ? $parameters['screen_shots_path'] : 'some/default/path';
}
public function takeScreenshotAfterFailedStep($event)
{
$directory = $this->screenShotsPath;
}
}

How to configure Symfony2 sticky locale during session

I would like to translate my website thanks to an link on the right top.
I found out that, since Symfony 2.1, the locale is not stored in the session anymore.
So, I followed this Symfony documentation: Making the Locale "Sticky" during a User's Session
...Bundle/Service/LocaleListener.php
class LocaleListener implements EventSubscriberInterface
{
private $defaultLocale;
public function __construct($defaultLocale)
{
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
$locale = $request->attributes->get('_locale');
var_dump($locale);
if ($locale) {
$request->getSession()->set('_locale', $locale);
} else {
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
}
static public function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
...Bundle/Resources/config/services.yml
locale_listener:
class: ..Bundle\Service\LocaleListener
arguments: ["%kernel.default_locale%"]
tags:
- { name: kernel.event_subscriber }
./app/config/config.yml
framework:
translator: { fallback: en }
And, I add two links to translate my website on the parent twig template, shown below (Symfony2 locale languages whole page event listener).
base.html.twig
<li><a href="{{-
path(app.request.get('_route'),
app.request.get('_route_params')|merge({'_locale' : 'fr'}))
-}}">FR</a></li>
<li><a href="{{-
path(app.request.get('_route'),
app.request.get('_route_params')|merge({'_locale' : 'en'}))
-}}">EN</a></li>
Problem and Question
When I click on one of these links, the parameter _locale is added.
For instance:
satisfaction?_locale=fr
So, the value of the _locale parameter is fr. Consequently, my website should be translated in french.
Nevertheless, that
var_dump($locale)
in the listener is displayed three times:
null
en
null
I don't understand why the _locale parameter is not found when it display null and why the en?
With your listener, you will catch all request and subrequest that is not needed. This explain the three times apparition.
Try to add this following code to your onKernelRequest method:
if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) {
return;
}
This will avoid subRequests and possibly resolve your problem.

Symfony2 - How to change a document using a postLoad event-listener with Doctrine MongoDB ODM?

I have a document which describes a model in my app,
I want to change a field value after the document is loaded, I figured out that the way to do it is with event listener.
I added a new listener (postLoad listener) in my config/services.yml, I can't figure out how to get the document and change it before it is being sent.
help would be appreciated. :)
This is what I added to config/service.yml (under service)
core.listener:
class: Matan\CoreBundle\EventListener\DocumentListener
tags:
- { name: doctrine_mongodb.odm.event_listener, event: postLoad }
DocumentListener.php
namespace Matan\CoreBundle\EventListener;
use Matan\CoreBundle\Document\App;
class DocumentListener
{
public function postLoad()
{
//I Want to change it here
}
}
solution:
You should specify the listener's method that shall be called in your service definition:
- { name: doctrine_mongodb.odm.event_listener, event: postLoad, method: onPostLoad }
Now you can get the document that has just been loaded from the EventArgs passed to the onPostLoad method.
check if it matches the model you want to change, then perform your changes.
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
use Acme\Your\Document\MyDocument;
public function onPostLoad(LifecycleEventArgs $eventArgs)
{
$document = $eventArgs->getDocument();
if !($document instanceof MyDocument) {
return;
}
// ... your code here
// $document->setLoaded(new \Date('now'));
}

Categories