How to customize exception by bundle?
Example:
I have two bundles: BackendBundle and FrontEndBundle. I want this two bundles to be handled by two different templates when error 404 is thrown.
How can I do that?
I had read http://symfony.com/doc/current/cookbook/controller/error_pages.html but still got no clues.
Like in the cookbook article mentioned, extend the TwigBundle and the Symfony\Bundle\TwigBundle\Controller\ExceptionController:findTemplate. There you can decide (if it's not in debug) which 404 to show.
This example assumes all you backend routes are reachable under /backend. Change it to your needs, or use other things from the request to determine your backend 404s.
namespace Acme\ErrorBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController as BaseController;
/**
* ExceptionController.
*/
class ExceptionController extends BaseController
{
/**
* #param Request $request
* #param string $format
* #param integer $code An HTTP response status code
* #param Boolean $debug
*
* #return TemplateReference
*/
protected function findTemplate(Request $request, $format, $code, $debug)
{
// find template for backend 404 errors
if (!$this->debug && 404 == $code && false !== strpos($request->getPathInfo(), '/backend')) {
$template = new TemplateReference('TwigBundle', 'Exception', 'backend404', $format, 'twig');
if ($this->templateExists($template)) {
return $template;
}
}
// the parent method finds the error404.html.twig for the frontend
return parent::findTemplate($request, $format, $code, $debug);
}
}
Also to mention, the ErrorBundle must inherit from the TwigBundle.
You can hook to the KernelEvents::EXCEPTION event and override the response that will be sent to the browser. I wrote a quick gist for you:
https://gist.github.com/bezhermoso/87716a9c72a1d12c5036
However, $event->getRequest()->get('_controller') will return null on 404 errors, obviously. So you have to account for that instance.
Related
I've created a RequestPolicy in src/Policy/RequestPolicy.php to allow access to all actions of my SuperRubriquesController only to a "super-admin" user :
namespace App\Policy;
use Authorization\Policy\RequestPolicyInterface;
use Cake\Http\ServerRequest;
use Authorization\IdentityInterface;
class RequestPolicy implements RequestPolicyInterface
{
/**
* Method to check if the request can be accessed
*
* #param \Authorization\IdentityInterface|null $identity Identity
* #param \Cake\Http\ServerRequest $request Server Request
* #return bool
*/
public function canAccess($identity, ServerRequest $request)
{
if ($request->getParam('controller') === 'SuperRubriques' && $identity) {
return $identity->role === 'super-admin';
}
return true;
}
}
It works fine when I go to "/super-rubriques/index" or others actions of SuperRubriquesController but I'm wondering if there's a way to check if a user can access to a request from a template.
For example, I'd like to check if user can access to action index of SuperRubriquesController before to display the link.
if ($this->request->getAttribute('identity')->can('access', $requestToSuperRubriquesIndex)) {
echo $this->Html->link('Super Rubriques', ['controller' => 'SuperRubriques', 'action' => 'index']);
}
How can I build $requestToSuperRubriquesIndex ?
One way would be to use the with* methods of the current request object to create a clone with modified data:
$requestToSuperRubriquesIndex = $this->request
->withParam('controller', 'SuperRubriques')
->withParam('action', 'index');
See also
API > \Cake\Http\ServerRequest::withParam()
I recently dove into the world of laravel (version 5.4). While initially confused, the concept of MVC makes a lot of sense in writing large applications. Applications that you want to be easily understood by outside developers.
Using laravel for this has greatly simplified coding in PHP and has made the language fun again. However, beyond dividing code into its respective models, views, and controllers, what happens if we need to divide controllers to prevent them from growing too large?
A solution that I have found to this is to define one controller each folder and then fill that controller with traits that further add functionalities to the controller. (All-caps = folder):
CONTROLLER
HOME
Controller.php
TRAITS
additionalFunctionality1.php
additionalFunctionality2.php
additionalFunctionality3.php
...
ADMIN
Controller.php
TRAITS
additionalFunctionality1.php
additionalFunctionality2.php
additionalFunctionality3.php
...
Within routes/web.php I woud initialize everything as so:
Route::namespace('Home')->group(function () {
Route::get('home', 'Controller.php#loadPage');
Route::post('test', 'Controller.php#fun1');
Route::post('test2', 'Controller.php#fun2');
Route::post('test3', 'Controller.php#fun3');
});
Route::namespace('Admin')->group(function () {
Route::get('Admin', 'Controller.php#loadPage');
Route::post('test', 'Controller.php#fun1');
Route::post('test2', 'Controller.php#fun2');
Route::post('test3', 'Controller.php#fun3');
});
With me being new to laravel, this seems like a simple and elegant way to organize my logic. It is however something I do not see while researching laravel controller organization.
The Question
Is there an issue, both in the short-run and in the long-run, of organizing my data like this? What is a better alternative?
Example Controller:
<?php
namespace App\Http\Controllers\Message;
use DB;
use Auth;
use Request;
use FileHelper;
use App\Http\Controllers\Message\Traits\MessageTypes;
use App\Http\Controllers\Controller;
class MessageController extends Controller
{
// Traits that are used within the message controller
use FileHelper, MessageTypes;
/**
* #var array $data Everything about the message is stored here
*/
protected $data = []; // everything about the message
/**
* #var booloean/array $sendableData Additional data that is registered through the send function
*/
protected $sendableData = false;
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('access');
}
/**
* Enable sendableData by passing data to the variable
*
* #param array $data Addition data that needs to registrered
* #return MessageController
*/
protected function send ($data = []) {
// enable sendableData by passing data to the variable
$this->sendableData = $data;
return $this;
}
/**
* Enable sendableData by passing data to the variable
*
* #param string $type The type of message that we will serve to the view
* #return MessageController
*/
protected function serve ($type = "message") {
$this->ss();
$this->setData(array_merge($this->sendableData, $this->status[$type]));
$this->data->id = DB::table('messages')->insertGetId((array) $this->data);
}
/**
* Set the data of the message to be used to send or construct a message
* Note that this function turns "(array) $data" into "(object) $data"
*
* #param array $extend Override default settings
* #return MessageController
*/
protected function setData(array $extend = []) {
$defaults = [
"lobby" => Request::get('lobbyid'),
"type" => "text",
"subtype" => null,
"body" => null,
"time" => date("g:ia"),
"user" => Auth::User()->username,
"userid" => Auth::User()->id,
"day" => date("j"),
"month" => date("M"),
"timestamp" => time(),
"private" => Request::get('isPrivate') ? "1" : "0",
"name" => Request::get('displayname'),
"kicker" => null
];
$this->data = (object) array_merge($defaults, $extend);
// because a closure can not be saved in the database we will remove it after we need it
unset($this->data->message);
return $this;
}
/**
* Send out a response for PHP
*
* #return string
*/
public function build() {
if($this->data->type == "file") {
$filesize = #filesize("uploads/" . $this->data->lobby . "/" . $this->data->body);
$this->data->filesize = $this->human_filesize($filesize, 2);
}
// do not send unneccessary data
unset($this->data->body, $this->data->time, $this->data->kicker, $this->data->name, $this->data->timestamp);
return $this->data;
}
/**
* Send out a usable response for an AJAX request
*
* #return object
*/
public function json() {
return json_encode($this->build());
}
}
?>
Laravel architecture is simple enough for any size of the application.
Laravel provides several mechanisms for developers to tackle the fatty controllers in your Application.
Use Middlewares for authentications.
Use Requests for validations and manipulating data.
Use Policy for your aplication roles.
Use Repository for writing your database queries.
Use Transformers for your APIs to transform data.
It depends on your application. if it is too large and have different Modules or functionalities then you should use a modular approach.
A nice package is available for making independent modules here
Hope this helps.
I think you should do a little differently ! First you should use your traits at the same levels as the controllers since traits are not controllers, your tree should look more like :
Http
Controller
Controller.php
Home
YourControllers
Admin
Your admin controllers
Traits
Your Traits
Next your routes need to be more like that :
Route::group(['prefix' => 'home'], function()
{
Route::get('/', 'Home\YourController#index')->name('home.index');
}
Route::group(['prefix' => 'admin', 'middleware' => ['admin']], function()
{
Route::get('/', 'Admin\DashboardController#index')->name('dashboard.index');
}
You can use many kink or routes like :
Route::post('/action', 'yourControllers#store')->name('controller.store');
Route::patch('/action', 'yourControllers#update')->name('controller.update');
Route::resource('/action', 'yourController');
The Resource route creates automatically the most used your, like post, patch, edit, index.. You just need to write the action and the controller called with its action. You can check out your toutes with this command : php artisan route:list
Laravel also has many automated features, like the creation of a controller with this command : php artisan make:controller YourController.
For the routes the prefix creates portions of url, for example all the routes inside the route group with the prefix 'admin' will lool like : www.yourwebsite.com/admin/theroute, and can also be blocked for some users with a middleware.
To get familiar with laravel i suggest you follow the laravel 5.4 tutorial from scratch by Jeffrey Way on Laracasts, he's awesome at explaining and showing how laravel works. Here is a link : https://laracasts.com/series/laravel-from-scratch-2017
Hope it helps, ask me if you want to know anything else or have some precisions, i'll try to answer you !
From following this example I have managed to set up the below Listener/Before Filter to parse all requests to my API endpoint (ie. /api/v1) before any controller actions are processed. This is used to validate the request type and to throw an error if certain conditions are not met.
I would like to differentiate the error response based on the applications environment state. If we are in development or testing, I simply want to throw the error encountered. Alternatively, if in production mode I want to return the error as a JSON response. Is this possible? If not, how could I go about something similar?
I'm using Symfony v3.1.*, if that makes any difference.
namespace AppBundle\EventListener;
use AppBundle\Interfaces\ApiAuthenticatedController;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class ApiBeforeListener
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Validates a request for API resources before serving content
*
* #param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
* #return mixed
*/
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
return;
if ($controller[0] instanceof ApiAuthenticatedController) {
$headers = $event->getRequest()->headers->all();
// only accept ajax requests
if(!isset($headers['x-requested-with'][0]) || strtolower($headers['x-requested-with'][0]) != 'xmlhttprequest') {
$error = new AccessDeniedHttpException('Unsupported request type.');
if (in_array($this->container->getParameter("kernel.environment"), ['dev', 'test'], true)) {
throw $error;
} else {
// return json response here for production environment
//
// For example:
//
// header('Content-Type: application/json');
//
// return json_encode([
// 'code' => $error->getCode(),
// 'status' => 'error',
// 'message' => $error->getMessage()
// ]);
}
}
}
}
}
Unlike most events, the FilterControllerEvent does not allow you to return a response object. Be nice if it did but oh well.
You have two basic choices.
The best one is to simply throw an exception and then add an exception listener. The exception listener can then return a JsonResponse based on the environment.
Another possibility to to create a controller which only returns your JsonResponse then use $event->setController($jsonErrorController) to point to it.
But I think throwing an exception is probably your best bet.
More details here: http://symfony.com/doc/current/reference/events.html
I have the following code in my AppServiceprovider::boot method, whcih ensures I get an email whenever anything is logged with a warning or greater severity level.
$message = \Swift_Message::newInstance( 'An Error has Occurred in XXX' )
->setTo( env('ERROR_EMAIL_TO') )
->setFrom( env('ERROR_EMAIL_FROM') )
->setReplyTo( env('ERROR_EMAIL_REPLY_TO') )
->setContentType( 'text/html' );
$swiftMailer = \Mail::getSwiftMailer();
$handler = new SwiftMailerHandler( $swiftMailer, $message, Logger::WARNING );
$handler->setFormatter( new HtmlFormatter() );
\Log::getMonolog()->pushHandler( $handler );
But while this works, I can't help but feel that it's in the wrong place.
Where would you add this code to a Laravel web app?
How about using middleware? I've written some middleware to log all requests, and responses in the past for APIs that could just as easily dispatch e-mails to inform a user of errors (this was pretty much the use case of why I set it up).
Using the terminate() method in your middleware class, will allow you to perform logic after a response has been sent to the user - so your e-mails shouldn't slow down the experience for the end user.
namespace App\Http\Middleware;
use Closure;
class LogRequestAndResponseMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
public function terminate($request, $response)
{
// Send out an e-mail to you here
}
I think this could be also allow you to a good time to refactor the code, which will help move the logic outside of the middleware and into its own area of responsibility.
In this instance, I'm thinking that I currently want to be informed via e-mail, but I may at some point in the future want to send an event via a Websocket instead.
Therefore, I'd wrap up the logic using a contract and implement it accordingly:
interface ErrorNotificationContract
{
public function inform($user, $message)
}
class EmailErrorNotification implements ErrorNotificationContract
{
protected $mail;
public function __construct(Mail $mail)
{
$this->mail = $mail;
}
public function inform($user, $message)
{
// Your send e-mail logic.
}
}
You can then register this using a service provider. A side effect is that you get the added benefits of:
Dependency injection in the EmailErrorNotification (better testability)
Better decoupled code
Implementations that can be changed very easily - just create a new class that implements the ErrorNotificationContract
In your middleware you could then do:
public function terminate($request, $response)
{
// ...
$errorNotifier->inform('youremail#domain.com', 'something bad happened');
}
I have a controller that i am trying to do a functional test for it.
controller:
<?php
namespace Zanox\AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Exception;
/**
*
* #author Mohamed Ragab Dahab <eng.mohamed.dahab#gmail.com>
*
* #Route("merchant")
*
*/
class ReportController extends Controller {
/**
* Show transaction report regarding to the given merchant ID
* #author Mohamed Ragab Dahab <eng.mohamed.dahab#gmail.com>
* #access public
*
* #Route("/{id}/report", name="merchant-report")
*
* #param int $id Merchant ID
*/
public function showAction($id) {
try {
//Order Service
$orderService = $this->get('zanox_app.orderService');
//merchant Orders
$orders = $orderService->getMerchantOrders($id);
//render view and pass orders array
return $this->render('ZanoxAppBundle:Report:show.html.twig', ['orders' => $orders]);
} catch (Exception $e) {
//log errors
}
}
}
I have created a functional test as following:
namespace Zanox\AppBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ReportControllerTest extends WebTestCase {
/**
*
*/
public function testShow() {
//Client instance
$client = static::createClient();
//Act like browsing the merchant listing page, via GET method
$crawler = $client->request('GET', '/merchant/{id}/report', ['id'=> 1]);
//Getting the response of the requested URL
$crawlerResponse = $client->getResponse();
//Assert that Page is loaded ok
$this->assertEquals(200, $crawlerResponse->getStatusCode());
//Assert that the response content contains 'Merchant Listing' text
$this->assertTrue($crawler->filter('html:contains("Merchant Report")')->count() > 0);
}
}
However this test fails as the first assertion returns status 500 instead of 200
Test log shows:
[2015-07-06 21:00:24] request.INFO: Matched route "merchant-report". {"route_parameters":{"_controller":"Zanox\AppBundle\Controller\ReportController::showAction","id":"{id}","_route":"merchant-report"},"request_uri":"http://localhost/merchant/{id}/report?id=1"} []
Letting you know that ['id' => 1] exists in DB.
First Question: why it fails?
Second Question: am i doing the functional test in a proper way?
If you look at the logs, you see that the {id} parameter is not correctly replaced but is added in the query string of your Uri. So try with:
$crawler = $client->request('GET', sprintf('/merchant/%d/report', 1));
When using GET, the third parameter will add query parameters for the URI, when using POST, these data will be posted.
As to why it fails - you can troubleshoot the problem by using a debugger to step through the controller code when it is executed in your test. For your second question, yes, you are doing a simple functional test correctly.