I have a question about Zendframework 2, Event manager and listener.
class ApiErrorListener extends AbstractListenerAggregate {
public function attach(EventManagerInterface $events)
{
$this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, __CLASS__ . '::onRender', 1000);
}
public static function onRender(MvcEvent $e)
{
if($e->getResponse()->isOk())
{
return;
}
$httpCode = $e->getResponse()->getStatusCode();
$sm = $e->getApplication()->getServiceManager();
$viewModel = $e->getResult();
$exception = $viewModel->getVariable('exception');
$model = new JsonModel(
array(
'errorCode' => !empty($exception) ? $exception->getCode() : $httpCode,
'errorMsg' => !empty($exception) ? $exception->getMessage() : NULL
)
);
$model->setTerminal(true);
$e->setResult($model);
$e->setViewModel($model);
$e->getResponse()->setStatusCode($httpCode);
}
}
I think ApiErrorListener should be a listener, or say it is an observer. Why it has to implement attach() function?
Observer Design Pattern
In this link, you can see, only Subject (broadcaster attach or detach listeners).
I think I get confused ...
Anyone please help.
Thanks in advance.
The AbstractListenerAggregate provides functionality to attach/detachs a group of events with similar scope and purpose.
It's more of an organizational class than functionality. It provides an easy way to attach and detach events. In the case of event detaching, it provides an easy way to detach a group of events without any complicated logic to loop over ALL registered events and find the ones you're looking for.
You don't have to use the listener aggregate, you can certainly attach events to the EventManager anywhere you want in an application. However, as your application grows and there are more and more events with more and more dependencies, it can provide some sanity (AND TESTABILITY) to your event logic.
Related
We use the Symfony Serializer to convert some Doctrine objects to JSON, goal is to provide them as results of API calls.
Out data model has about thirty classes, all are somehow linked with each other and the Doctrine model reflects this. So navigation from one instance to other linked instances is easy.
Now, we are pretty happy that no changes are necessary when we add a new attribute to a class. However, the situation is different is a new link is added, this often adds way too much information because the linked classes also have links and they have links and arrays of objects...
To restrict this, we typically add ignored attributes:
protected function serialize($e, $ignored = ['user'])
{
if ($this->container->has('serializer')) {
$this->serializer = $this->container->get('serializer');
} else {
throw new ServiceNotFoundException('Serializer not found');
}
return $this->serializer->serialize($e, 'json', [
'ignored_attributes' => $ignored,
ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
if (method_exists($object, 'getUuid')) {
return $object->getUuid();
} elseif (method_exists($object, 'getId')) {
return $object->getId();
} elseif (method_exists($object, '__toString')) {
return $object->__toString();
} else {
return '[Reference to object of type ' . get_class($object) . ']';
}
},
]);
}
And then:
return new Response($this->serialize(
[
'presentation' => $presentation,
],
[
'zoomUser',
'userExcelDownloads',
'presentationUserTopics',
'addedBy',
'user',
'presentations',
'sponsorScopes',
'sponsorPresentations',
'showroomScope',
'presentationsForTopic',
'presentationsForTopics',
'presentationHistories',
'showroomTopics',
'presentation',
'presentationHistory',
]
));
This works but maintenance is horrible - when ever the database model is changed, there is the risk that an API call emits a few more MB because a new attribute would have to be ignored. There is no way of finding this.
So how do you handle this?
One solution could be to restrict the serialization depth similar to the CIRCULAR_REFERENCE_HANDLER, i.e., for objects on level three just add the IDs and not the full objects. How could I build this?
Symfony serializer has built-in Ignore strategy (https://symfony.com/doc/current/components/serializer.html#ignoring-attributes)
you can ignore the attribute directly from the model.
use Symfony\Component\Serializer\Annotation\Ignore;
class Presentation
{
/**
* #Ignore()
*/
public $zoomUser;
//...
}
or by using context.
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
$normalizer = new ObjectNormalizer();
$encoder = new JsonEncoder();
$serializer = new Serializer([$normalizer], [$encoder]);
$serializer->serialize($presentation, 'json', [AbstractNormalizer::IGNORED_ATTRIBUTES => ['zoomUser']]);
We switched to JMS Serializer Bundle where setting the max. depth is very simple and helps us a lot.
https://jmsyst.com/bundles/JMSSerializerBundle
For Symfony serializer, the only way is to use serialization groups.
First off, I am building using Symfony components. I am using 3.4. I was following the form tutorial https://symfony.com/doc/3.4/components/form.html which lead me to this page
https://symfony.com/doc/current/forms.html#usage
I noticed that Symfony added a Form directory to my application.
This was great! I thought I was on my way. So, in my controller, I added this line.
$form = Forms::createFormFactory();
When I tried loading the page, everything went well with no error messages until I added the next two lines.
->addExtension(new HttpFoundationExtension())
->getForm();
I removed the ->addExtension(new HttpFoundationExtension()) line and left the ->getForm() thinking it would process without the add method call. It did not. So, I backed up to see if the IDE would type hint for me.
In the IDE PHPStorm, these are the methods that I have access to but not getForm per the tutorial
Every tutorial I have tried ends with not being able to find some method that does not exist. What do I need to install in order to have access to the ->getForm() method?
UPDATE:
I have made a couple of steps forward.
$form = Forms::createFormFactory()
->createBuilder(TaskType::class);
The code above loads with no errors. (Why is still fuzzy). But next stop is the createView(). None existant also. I only get hinted with create().
Reading between the lines in this video help with the last two steps. https://symfonycasts.com/screencast/symfony3-forms/render-form-bootstrap#play
UPDATE 2:
This is what I have now.
$session = new Session();
$csrfManager = new CsrfTokenManager();
$help = new \Twig_ExtensionInterface();
$formFactory = Forms::createFormFactoryBuilder()
->getFormFactory();
$form = $formFactory->createBuilder(TaskType::class)
->getForm();
//$form->handleRequest();
$loader = new FilesystemLoader('../../templates/billing');
$twig = new Environment($loader, [
'debug' => true,
]);
$twig->addExtension(new HeaderExtension());
$twig->addExtension(new DebugExtension());
$twig->addExtension($help, FormRendererEngineInterface::class);
return $twig->render('requeueCharge.html.twig', [
'payments' => 'Charge',
'reportForm' => $form->createView()
]);
Does anyone know of an update standalone for example? The one that everyone keeps pointing two is 6 years old. There have been many things deprecated in that time period. So, it is not an example to follow.
Your Form class and method createFormFactory must return object that implement FormBuilderInterface then getForm method will be available. You need create formBuilder object.
But this can't be called from static method because formBuilder object need dependency from DI container. Look at controller.
If you want you need register your own class in DI and create formBuilder object with dependencies and return that instance of object.
EDIT
You don't need to use abstract controller. You can create your own class which is registered in DI for geting dependencies. In that class you create method which create new FormBuilder(..dependencies from DI from your class ...) and return instance of that FormBuilder. Then you can inject your class in controller via DI.
Example (not tested)
// class registered in DI
class CustomFormFactory
{
private $_factory;
private $_dispatcher;
public CustomFormFactory(EventDispatcherInterface $dispatcher, FormFactoryInterface $factory)
{
$_dispatcher = $dispatcher;
$_factory = $factory;
}
public function createForm(?string $name, ?string $dataClass, array $options = []): FormBuilderInterface
{
// create instance in combination with DI dependencies (factory..) and your parameters
return new FormBuilder($name, $dataClass, $_dispatcher, $_factory, $options);
}
}
Usage
$factory = $this->container->get('CustomFormFactory');
$fb = $factory->createForm();
$form = $fb->getForm();
I started using zend 3 a few months ago for a project and now I'm stuck.
I have a customized authentication (not using zend authentication module) which is working fine but I need to validate every time i access a redirected page.
Because on every page's url goes a token that is used to check in database, and I'm trying to do inside the function onBootStrap().
I learned to use factories, models, mappers and I'm currently using them in some controllers, but i can't find a way to achieve, at least if i could get the dbAdapter from the bootstrap event to use, it will be enough.
Any thoughts?
You can use lazy event for this job. here's you are a simple example.
public function onBootstrap(EventInterface $e)
{
/** #var \Interop\Container\ContainerInterface $container */
$container = $e->getApplication()->getserviceManager();
$events = $e->getApplication()->getEventManager();
$events->attach(MvcEvent::EVENT_ROUTE, new LazyListener([
'listener' => Listener::class,
'method' => 'onRoute'
], $container));
}
So you can check you auth on Listener class' "onRoute" method. If MvcEvent::ROUTE event is too early for you, you can use other MvcEvents too.
Hope this can solve your problem.
To get the adapter and use it in tablegateway
public function onBootstrap(EventInterface $event){
$container = $event->getApplication()->getServiceManager();
$dbAdapter = $container->get(AdapterInterface::class);
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new TableObjectClass());
$tableGateway = new TableGateway("Table name", $dbAdapter, null, $resultSetPrototype);
$mapper = new TableObjectClassMapper($tableGateway);
//and use $mapper to get data from the table and store it as TableObjectClass
$data = $mapper->fetch()->current();
}
As far as I understand the valid pattern is:
a FooControllerFactory that instantiates the needed service(s) (FooService)
a FooController with constructor __construct(FooService $fooService)
the Controller acquires some basic data and gets a result from the service
the Service contains all the required business logic
This is a base service. Eventualy this service will need other services for various activities.
For example CacheService, SomeOtherDataService.
The question is, what is a valid/appropriate pattern for
including/injecting those other interconnected services?
A reallife example of that we have currently, extremely simplified:
AuctionController
/**
* get vehicles for specific auction
*/
public function getVehiclesAction ()
{
$auctionService = $this->getAuctionService(); // via service locator
$auctionID = (int) $this->params('auction-id');
$auction = $auctionService->getAuctionVehicle($auctionID);
return $auction->getVehicles();
}
AuctionService
public function getAuctionVehicles($auctionID) {
$auction = $this->getAuction($auctionID);
// verify auction (active, permissions, ...)
if ($auction) {
$vehicleService = $this->getVehicleService(); // via service locator
$vehicleService->getVehicles($params); // $params = some various conditions or array of IDs
}
return false;
}
VehicleService
public function getVehicles($params) {
$cache = $this->getCache(); // via service locator
$vehicles = $cache->getItem($params);
if (!$vehicles) {
$vehicleDB = $this->getVehicleDB(); // via service locator
$vehicles = $vehicleDB->getVehicles($params);
}
return $vehicles;
}
Example of a suggested valid pattern
AuctionController
public function __construct(AuctionService $auctionService) {
$this->auctionService = $auctionService;
}
/**
* get vehicles for specific auction
*/
public function getVehiclesAction ()
{
$auctionID = (int) $this->params('auction-id');
$auction = $this->auctionService->getAuctionVehicle($auctionID);
return $auction->getVehicles();
}
**AuctionService**
public function getAuctionVehicles($auctionID) {
$auction = $this->getAuction($auctionID); // no problem, local function
// verify auction (active, permissions, ...)
if ($auction) {
$vehicleService = $this->getVehicleService(); // we don't have service locator
$vehicleService->getVehicles($params); // $params = some various conditions or array of IDs
}
return false;
}
VehicleService
public function getVehicles($params) {
$cache = $this->getCache(); // we don't have service locator, but cache is probably static?
$vehicles = $cache->getItem($params);
if (!$vehicles) {
$vehicleDB = $this->getVehicleDB(); // where and how do we get this service
$vehicles = $vehicleDB->getVehicles($params);
}
return $vehicles;
}
Some notes:
Services are interconnected only in some cases, in 95% they are standalone
Auction has a lot funcionality that does not need Vehicle
Vehicle has VehicleController and VehicleService that does only in some cases relate do Auction, it's a standalone module that has other functionalities
The Injection of every needed service in a controller would be a waste of resources, because they are not needed in every action (in the real-life application we have many more interconnected services, not just two)
Programming the same business logic in multiple services just to avoid the service locator is obviously an invalid pattern and not acceptable.
If a controller requires too many different services, it usually is an indicator that the controller has too many responsibilities.
Following up on #AlexP's answer, this service then would be injected in your controller. Depending on your setup, this sure can result in dependecy injection cascades when a controller is created. This at least will limit the created services to those that are actually required by the controller (and those related transitively).
If some of these services are only required rarely and you are worried about creating them all on each request, the new Service Manager now supports lazy services too. Those still can be injected into a service / controller as a regular dependency (as above), but are only created when called for the first time.
Copying this from the documentation's example:
$serviceManager = new \Zend\ServiceManager\ServiceManager([
'factories' => [
Buzzer::class => InvokableFactory::class,
],
'lazy_services' => [
// Mapping services to their class names is required
// since the ServiceManager is not a declarative DIC.
'class_map' => [
Buzzer::class => Buzzer::class,
],
],
'delegators' => [
Buzzer::class => [
LazyServiceFactory::class,
],
],
]);
When requesting the service, it does not get created right away:
$buzzer = $serviceManager->get(Buzzer::class);
But only when it is first used:
$buzzer->buz();
This way you can inject multiple dependencies into your controller and only the services actually required will be created. Of course this is true for any dependency, like Services required by other services and so on.
You could compose a new service, say VehicleAuctionService and have both the AuctionService and VehicleService injected as dependancies using a factory.
This is object composition.
class VehicleAuctionService
{
private $auctionService;
private $vehicleService;
public function __construct(
AuctionService $auctionService,
VehicleService $vehicleService
){
$this->auctionService = $auctionService;
$this->vehicleService = $vehicleService;
}
public function getAuctionVehicles($auctionID)
{
$auction = $this->auctionService->getAuction($auctionID);
if ($auction) {
$params = [
'foo' => 'bar',
];
$this->vehicleService->getVehicles($params);
}
return false;
}
}
I have a questiom regarding the Zend Framework 2:
I have
library/System and library/Zend. the system is my custom library, which I want to configure de aplication (routes, modules, etc., and redirect user to correct module, controller and/or action).
I don't want to do this inside each application/modules/ModuleName/Module.php file. So, my library/System can do everything related to application configuration.
As said in the comments above: register to the bootstrap-event and add new routes there:
<?php
namespace Application;
use Zend\Module\Manager,
Zend\EventManager\StaticEventManager;
class Module
{
public function init(Manager $moduleManager)
{
$events = StaticEventManager::getInstance();
$events->attach('bootstrap', 'bootstrap', array($this, 'initCustom'), 100);
}
public function initCustom($e)
{
$app = $e->getParam('application');
$r = \Zend\Mvc\Router\Http\Segment::factory(array(
'route' => '/test',
'defaults' => array(
'controller' => 'test'
)
)
);
$app->getRouter()->addRoute('test',$r);
}
}
$app = $e->getParam('application'); does return an instance of Zend\Mvc\Application. Have a look there to see which additional parts you can get there. The bootstrap event is fired before the actual dispatching does happen.
Note that the ZendFramework 1 routes are not always compatible to the ZendFramework 2 ones.
Update to comments
public function initCustom($e)
{
$app = $e->getParam('application');
// Init a new router object and add your own routes only
$app->setRouter($newRouter);
}
Update to new question
<?php
namespace Application;
use Zend\Module\Manager,
Zend\EventManager\StaticEventManager;
class Module
{
public function init(Manager $moduleManager)
{
$events = StaticEventManager::getInstance();
$events->attach('bootstrap', 'bootstrap', array($this, 'initCustom'), 100);
}
public function initCustom($e)
{
$zendApplication = $e->getParam('application');
$customApplication = new System\Application();
$customApplication->initRoutes($zendApplication->getRouter());
// ... other init stuff of your custom application
}
}
This only happens in one zf2 module (named Application which can be the only one as well). This doesn't fit your needs? You could:
extend a custom module autoloader
extend Zend\Mvc\Application for your own logic
make your code zf2-compatible