How should I write in log file the error or the message I want to from an Entity class?
The idea is like this: I have some items with some properties and also a configuration with some properties. I need to check if the item has the property that also exists in the configuration properties. When I debug my application, at some point I am here:
public function getProperty(idProperty $propertyId)
{
$properties = $this->ItemProperties();
if (isset($properties[$propertyId->getValue()])) {
return $properties[$propertyId->getValue()];
}else{
//here I want to write in the log file that the propertyId is not in the $properties.
}
return null;
}
So how can I achieve that? Thank you.
You can throw Exception and setup Exception Listener, which will write into log.
Inside Entity:
if (isset($properties[$propertyId->getValue()])) {
return $properties[$propertyId->getValue()];
} else {
throw new DomainException('Something is wrong.' . print_r($this, true));
}
In Listener class:
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$e = $event->getException();
if ($e instanceof DomainException) {
$this->logger->warning('Exception ' . get_class($e) , ['message' => $e->getMessage()]);
$event->setResponse(
new JsonResponse(['error' => $e->getMessage()], 400)
);
services.yml
app.exception_listener:
class: Application\Listeners\ExceptionListener
arguments: ['#domain.logger']
tags:
- { name: kernel.event_listener, event: kernel.exception }
Symfony Events.
You can also inject Logger into your Entity and write to log from there, though it violates Single Responsibility Principle.
UPDATE
DomainException is a simple class, inheriting from \Exception. In this example it only exists to distinguish between your custom exceptions and those that are thrown by PHP or other libraries.
It can also contain additional functionality, for example, accepting two messages in constructor, writing one of them into log file and outputting another for your user.
Related
I am migrating legacy project routing (Yii1) to Symfony 5
Right now my config/routing.yaml looks something like this:
- {path: '/login', methods: ['GET'], controller: 'App\Controller\RestController::actionLogin'}
- {path: '/logout', methods: ['GET'], controller: 'App\Controller\RestController::actionLogout'}
# [...]
- {path: '/readme', methods: ['GET'], controller: 'App\Controller\RestController::actionReadme'}
As you can see there is plenty of repetitive url to action conversion.
Is it possible to dynamically resolve controller method depending on some parameter. E.g.
- {path: '/{action<login|logout|...|readme>}', methods: ['GET'], controller: 'App\Controller\RestController::action<action>'}
One option would be to write annotations, but that somehow does not work for me and throws Route.php not found
The controller is determined by a RequestListener, specifically the router RouterListener. This in turn uses UrlMatcher to check the uri against the RouteCollection. You could implement a Matcher that resolves the controller based on the route. All you have to do is return an array with a _controller key.
Take note that this solution won't allow you to generate a url from a route name, since that's a different Interface, but you could wire it together.
// src/Routing/NaiveRequestMatcher
namespace App\Routing;
use App\Controller\RestController;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\RequestContext;
class NaiveRequestMatcher implements UrlMatcherInterface
{
private $matcher;
/**
* #param $matcher The original 'router' service (implements UrlMatcher)
*/
public function __construct($matcher)
{
$this->matcher = $matcher;
}
public function setContext(RequestContext $context)
{
return $this->matcher->setContext($context);
}
public function getContext()
{
return $this->matcher->getContext();
}
public function match(string $pathinfo)
{
try {
// Check if the route is already defined
return $this->matcher->match($pathinfo);
} catch (ResourceNotFoundException $resourceNotFoundException) {
// Allow only GET requests
if ('GET' != $this->getContext()->getMethod()) {
throw $resourceNotFoundException;
}
// Get the first component of the uri
$routeName = current(explode('/', ltrim($pathinfo, '/')));
// Check that the method is available...
$baseControllerClass = RestController::class;
$controller = $baseControllerClass.'::action'.ucfirst($routeName);
if (is_callable($controller)) {
return [
'_controller' => $controller,
];
}
// Or bail
throw $resourceNotFoundException;
}
}
}
Now you need to override the Listener configuration:
// config/services.yaml
Symfony\Component\HttpKernel\EventListener\RouterListener:
arguments:
- '#App\Routing\NaiveRequestMatcher'
App\Routing\NaiveRequestMatcher:
arguments:
- '#router.default'
Not sure if it's the best approach, but seems the simpler one. The other option that comes to mind is to hook into the RouteCompiler itself.
I would like to access my database that contains all my user inside my provider with doctrine. I followed a tutorial (http://symfony.com/doc/current/security/custom_provider.html) to build my provider for my user, so I have an loadUserByUsername function :
public function loadUserByUsername($username)
{
// make a call to your webservice here
$player = new Player();
$player = $this->getDoctrine()
->getRepository('AppBundle:Player')
->findOneByPseudo($username);
// pretend it returns an array on success, false if there is no user
if ($player) {
return $player;
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
But of course my getDoctrine() function is undefined. So there is something I don't understand with the provider, I am trying to use it to be authenticated when I login so I need a provider, but why I can't search inside my database? How should I write this function? Thank for your help
EDIT :
When I add doctrine by service.yml (and after writting my constructor inside my provider), I have this error :
FatalThrowableError in PlayerProvider.php line 13:
Type error: Argument 1 passed to AppBundle\Security\PlayerProvider::__construct() must be an instance of Doctrine\Bundle\DoctrineBundle\Registry, instance of Doctrine\ORM\EntityManager given, called in /home/jean/PW6/SkA/SkeletonsOnlineV2/skeleton-online/var/cache/dev/appDevDebugProjectContainer.php on line 327
EDIT 2 : When I just put arguments: ['#doctrine'] inside my service.yml, I get an error that says that doctrine is undefined
EDIT 3 : It works now, I just made a dumb mistake
If you read further, it says the following (emphasis mine):
The real implementation of the user provider will probably have some dependencies or configuration options or other services. Add these as arguments in the service definition.
So in your case it would be something like
# app/config/services.yml
services:
app.webservice_user_provider:
class: AppBundle\Security\User\WebserviceUserProvider
arguments: ['#doctrine']
And your class needs a constructor
class WebserviceUserProvider implements UserProviderInterface
{
protected $doctrine;
public function __construct (\Doctrine\Bundle\DoctrineBundle\Registry $doctrine)
{
$this->doctrine = $doctrine;
}
// ...
}
Then in your method replace $this->getDoctrine() with just $this->doctine
I am currently migrating an existent application to Symfony2 that has about 100 controllers with approximately 8 actions in each controller. All the current Actions are named as follow:
public function index(){}
However the default naming convention for Symfony is indexAction().
Is it possible to keep all my current actions and tell Symfony to use as it is without the "Action" word after the method name?
thank you.
Yes, this is possible. You should be able to define routes as normal, but you need to change the way the kernel finds the controller. The best way to do this is to replace/decorate/extends the service 'controller_name_converter'. This is a private service and is injected into the 'controller_resolver' service.
The source code of the class you want to replace is at 'Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser'.
Basically, the code runs like this. The 'bundle:controller:action' you specified when creating the route is saved in the cache. When a route is matched, that string is given back to the kernel, which in turn calls 'controller_resolver' which calls 'controller_name_resolver'. This class convert the string into a "namespace::method" notation.
Take a look at decorating services to get an idea of how to do it.
Here is an untested class you can work with
class ActionlessNameParser
{
protected $parser;
public function __construct(ControllerNameParser $parser)
{
$this->parser = $parser;
}
public function parse($controller)
{
if (3 === count($parts = explode(':', $controller))) {
list($bundle, $controller, $action) = $parts;
$controller = str_replace('/', '\\', $controller);
try {
// this throws an exception if there is no such bundle
$allBundles = $this->kernel->getBundle($bundle, false);
} catch (\InvalidArgumentException $e) {
return $this->parser->parse($controller);
}
foreach ($allBundles as $b) {
$try = $b->getNamespace().'\\Controller\\'.$controller.'Controller';
if (class_exists($try)) {
// You can also try testing if the action method exists.
return $try.'::'.$action;
}
}
}
return $this->parser->parse($controller);
}
public function build($controller)
{
return $this->parser->build($controller);
}
}
And replace the original service like:
actionless_name_parser:
public: false
class: My\Namespace\ActionlessNameParser
decorates: controller_name_converter
arguments: ["#actionless_name_parser.inner"]
Apparently the Action suffix is here to distinguish between internal methods and methods that are mapped to routes. (According to this question).
The best way to know for sure is to try.
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class HelloController
{
/**
* #Route("/hello/{name}", name="hello")
*/
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
Try to remove the Action from the method name and see what happens.
I'm trying to make custom routeloader according to http://symfony.com/doc/current/cookbook/routing/custom_route_loader.html
my code looks like this
//the routeloader:
//the namespace and use code ....
class FooLoader extends Loader{
private $loaded = false;
private $service;
public function __construct($service){
$this->service = $service;
}
public function load($resource, $type=null){
if (true === $this->loaded)
throw new \RuntimeException('xmlRouteLoader is already loaded');
//process some routes and make $routeCollection
$this->loaded = true;
return $routeCollection;
}
public function getResolver()
{
// needed, but can be blank, unless you want to load other resources
// and if you do, using the Loader base class is easier (see below)
}
public function setResolver(LoaderResolverInterface $resolver)
{
// same as above
}
function supports($resource, $type = null){
return $type === 'xmlmenu';
}
}
//the service definition
foo.xml_router:
class: "%route_loader.class%"
arguments: [#foo.bar_service] //this service and the injection has been tested and works.
tags:
- { name: routing.loader }
//the routing definitions
//routing_dev.yml
_foo:
resource: "#FooBarBundle/Resources/config/routing.yml"
-----------------------------
//FooBarBundle/Resources/config/routing.yml
_xml_routes:
resource: .
type: xmlmenu
and when I try to access any route I get the exception:
RuntimeException: xmlRouteLoader is already loaded
which is the exception I defined if the loader is loaded multiple times.So why does it try to load this loader more than once? and I'm pretty sure I've defined it only there.
Actually the answer was quite simple.it seems like this method only supports one level of imports.I only needed to put the _xml_routes directly under routing_dev.yml, otherwise it somehow winds out in a loop.explanations to why that is are appreciated.
Exact same problem as unanswered question Symfony 2.4: Why are 500 errors not caught by kernel.exception listener.
I have implemented a custom exception "handler" which works awesomely for 403 and 404 issues, but 500 errors (which is what I REALLY want to handle, as I want to send an email to myself from the system when this happens) does not trigger my custom "handler" and continues to behave as if the custom "handler" was not there. The code is relatively straight forward:
Extract from app/config/config.yml:
services:
core.exceptlistener:
class: Pmb\LicensingBundle\Listener\ExceptionListener
arguments: ["#service_container", "#router"]
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException, priority: 200 }
Entire \Pmb\LicensingBundle\Listener\ExceptionListener.php:
<?php
namespace Pmb\LicensingBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\TwigBundle\TwigEngine;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class ExceptionListener
{
private $container;
private $router;
function __construct($container, $router)
{
$this->container = $container;
$this->router = $router;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
die("test");
$request = $this->container->get('request');
$templating = $this->container->get('templating');
if ($exception instanceof \Symfony\Component\Security\Core\Exception\AccessDeniedException)
{
# If AJAX request, do show not error page.
if ($request->isXmlHttpRequest())
{
$response = new Response(json_encode(array('error' => 'Access Denied: Not logged in as administrator')));
}
else
{
return new RedirectResponse($this->router->generate('login'));
}
$event->setResponse($response);
}
elseif ($exception->getStatusCode() == 404)
{
# If AJAX request, do show not error page.
if ($request->isXmlHttpRequest())
{
$response = new Response(json_encode(array('error' => 'Requested route could not be found')));
}
else
{
$response = new Response($templating->render('PmbLicensingBundle:Exception:error404.html.twig', array(
'exception' => $exception
)));
}
$event->setResponse($response);
}
else
{
# TODO: Send Email
# If AJAX request, do not show error page.
if ($request->isXmlHttpRequest()) $response = new Response(json_encode(array('error' => 'Internal Server Error Encountered. Developer has been notified.')));
else
{
$response = new Response($templating->render('PmbLicensingBundle:Exception:error500.html.twig', array(
'exception' => $exception
)));
}
}
}
}
I put the die("test") in there to verify that the "handler" is not being called at all and that is not just a problem with my if-else logic. As stated before, this works awesomely with any 404 or 403 errors, but 500 errors completely ignores this and behaves in the default manner. I am pretty sure that this has to do with the registering of the listener service, but I cannot find anything that explains how to make it work properly.
EDIT: Below are screenshots of errors that are not being handled as expected. I notice on the one (undefined variable) the die("test"); actually displays at the bottom, but on the ohter (syntax error) it does not display, even though both seems like they are being caught by Symfony2. Further testing showed that die("test"); showed up, but die(">> .$exception->getStatusCode()." <<"); did not. I am assuming that this is causing a second exception which I am not seeing, just like AccessDeniedException did not have this function call and I had to use instanceof, but there are many possible errors that can come up with a 500 error, so how do I distinguish whether the error is one of these?
FURTHER EDIT: On the error that did print the "Test" at the bottom, I noticed that when I do $container->testError();, I do not get the "Test", at the bottom of the error, but when I do return $container;, I do, even though both errors are ContextErrorException with undefined variable.
The 500 errors you got most probably before Symfony has chance to start. Since Symfony doesn't start in this case it cannot handle errors. To debug such cases, you should look at web server (apache, ngnix, etc) logs.
I just tested catching 500 exceptions and it seems to work fine. I think you may have something else intercepting the exception? Perhaps start a new project and try it again.
I myself prefer the subscriber interface:
class ExceptionListener extends ContainerAware implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(KernelEvents::EXCEPTION => array(
array('doException', 200),
));
}
public function doException(GetResponseForExceptionEvent $doEvent)
{
$exception = $doEvent->getException();
die('Exception caught ' . $exception->getStatusCode());
cerad_core__exception_listener:
class: Cerad\Bundle\CoreBundle\EventListener\ExceptionListener
tags:
- { name: kernel.event_subscriber }
Be aware that you can also configure the logger to send emails. Might be easier.
http://symfony.com/doc/current/cookbook/logging/monolog_email.html
#Ferhad makes a good point. Some (but not all) 500 errors will basically crash the S2 app. But I am assuming that you are getting the default S2 exception message.