I am making laravel app that has a CMS with pages stored in the database. A page record in the database has an id, title, content and url slug.
I want it so that if someone visits the url slug then it will load a page to display the content of that page on. So I think I need to override the 404 handler so that say the user goes to mysite.com/about before it 404's it checks the database to see if there is a record with about stored as the url slug. If there is then it will load the page to show the content, or if not then it will 404. I'm 90% of the way there with the following solution:
I have modified the render method in App\Exceptions\handler.php to be the following:
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if ($e instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
$page = Page::where('url', '=', Request::segment(1))->first();
if(!is_null($page)) {
return response(view('page', [ 'page' => $page ]));
}
}
return parent::render($request, $e);
}
This works however there is a problem, all of my menus are stored in the database as well and I have a made FrontendController that gets all of the menus from the database to be loaded into the views. All of my controllers extend FrontendController so that this is done on every page load and I don't have to repeat any code. So is there a way that I can route all 404s through a controller instead so that it can extend my FrontendController? Or am I just going to have to repeat all of the logic that the front end controller does in the exceptions handler file?
Solved it! Thanks to Styphon for giving me the idea, I just defined the following route as my very last route:
Route::get('/{url}', 'FrontendController#page');
Related
I have a main site and an admin control panel.
I want to have different 404 pages for each version.
How should I do this? I currently have the following code in my app/Exceptions/Handles.php file:
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
if($exception instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException)
{
$view = $request->is('admin/*') ? 'acp.errors.404' : 'errors.404' ;
return response()->view($view, [], 404);
}
return parent::render($request, $exception);
}
But I use the package spatie/laravel-permission and get the following error;
Trying to get property 'role' of non-object (View: F:\Development\RPR\site\resources\views\layouts\acp.blade.php) (View: F:\Development\RPR\site\resources\views\layouts\acp.blade.php)
I use in acp.blade.php auth()->user()->role, to get the user role, which just works fine without any exception. How should I fix this?
Here are two ways to accomplish different 404 views depending on the route. Both will allow you to have these error pages:
/resources/views/acp/errors/404.blade.php
/resources/views/errors/404.blade.php
The directories will be checked in order until a view is found, which means you can selectively add custom error views and fall through to the default when none exist. If the route did not match, then it will not look for a custom error page.
Option 1
Override registerErrorViewPaths() inside app/Exceptions/Handler.php:
/**
* Register the error template hint paths.
*
* #return void
*/
protected function registerErrorViewPaths()
{
parent::registerErrorViewPaths();
if (request()->is('admin/*')) {
View::prependNamespace(
'errors',
realpath(base_path('resources/views/acp/errors'))
);
}
}
Option 2
Create a ViewServiceProvider:
php artisan make:provider ViewServiceProvider
Register your provider in config/app.php:
'providers' => [
// ...
App\Providers\ViewServiceProvider::class,
],
Edit the boot method of your provider:
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
if (request()->is('admin/*')) {
View::prependNamespace(
'errors',
realpath(base_path('resources/views/acp/errors'))
);
}
}
For the second part of the question, auth()->user() is only available when the session middleware has run. If the 404 was caused by the route not existing, then the request does not go through the web middleware and unfortunately sessions and auth information will not be available. However, if the 404 was caused by a ModelNotFoundException triggered inside a controller, then the web middleware probably did run and you can access the user.
Inside your error view you have to check if the user is signed in:
#guest
<p>Hello, guest</p>
#else
<p>Hello, {{ auth()->user()->name }}</p>
#endguest
If this is not good enough for your use case, then you might want to try Route::fallback(), which allows you to define a controller for serving 404 pages and does run web middleware.
I created customs Twig templates for http error display to keep the site design unified by extending my base layout. (I want to keep my navigation menu and display the error, unlike the regular error messages)
It's working as expected but for the 404.
In the navigation menu of my base layout, I have a lot of is_granted('SOME_ROLES') to display the availables sections of the site depending of user's rights. When a 404 is thrown, the navigation menu is displayed as if the user is disconnected : {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %} being false.
After some searches, I found that the router is executed before the firewall. Since no route is found when a 404 is thrown, the firewall isn't executed and the rights aren't send to the template.
The only workaround I found (source from 2014) is to add at the very bottom of the routes.yaml file this route definition :
pageNotFound:
path: /{path}
defaults:
_controller: App\Exception\PageNotFound::pageNotFound
Since every other routes hasn't match, this one should be the not found.
The controller :
<?php
namespace App\Exception;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class PageNotFound
{
public function pageNotFound()
{
return (new NotFoundHttpException());
}
}
Because a controller is executed, the firewall is executed and the 404 error page is shown as I expected (hooray !).
My question is : Is there any proper way to fix that issue instead of that workaround?
We had a similar issue.
We wanted to have access to an authentication token in error pages.
In the scenario where part of the website is behind a firewall, say example.com/supersecretarea/, we wanted than unauthorized users get a 403 error code when accessing any url behind example.com/supersecretarea/, even in the event that the page doesn't exist. Symfony's behavior does not allow that and checks for a 404 (either because there is no route or because the route has parameter which didn't resolve, like example.com/supersecretarea/user/198 when the is no user 198).
What we ended up doing was to override the default router in Symfony (Symfony\Bundle\FrameworkBundle\Routing\Router) to modify its behavior:
public function matchRequest(Request $request): array
{
try {
return parent::matchRequest($request);
} catch (ResourceNotFoundException $e) {
// Ignore this next line for now
// $this->targetPathSavingStatus->disableSaveTargetPath();
return [
'_controller' => 'App\Controller\CatchAllController::catchAll',
'_route' => 'catch_all'
];
}
}
CatchAllController simply renders the 404 error page:
public function catchAll(): Response
{
return new Response(
$this->templating->render('bundles/TwigBundle/Exception/error404.html.twig'),
Response::HTTP_NOT_FOUND
);
}
What happens is that during the regular process of Symfony's router, if something should trigger a 404 error, we catch that exception within the matchRequest function. This function is supposed to return information about which controller action to run to render the page, so that's what we do: we tell the router that we want to render a 404 page (with a 404 code). All the security is handled in between matchRequest returning and catchAll being called, so firewalls get to trigger 403 errors, we have an authentication token, etc.
There is at least one functional issue to this approach (that we managed to fix for now). Symfony has an optional system that remembers the last page you tried to load, so that if you get redirected to the login page and successfully log in, you'll be redirected to that page you were trying to load initially. When the firewall throws an exception, this occurs:
// Symfony\Component\Security\Http\Firewall\ExceptionListener
protected function setTargetPath(Request $request)
{
// session isn't required when using HTTP basic authentication mechanism for example
if ($request->hasSession() && $request->isMethodSafe(false) && !$request->isXmlHttpRequest()) {
$this->saveTargetPath($request->getSession(), $this->providerKey, $request->getUri());
}
}
But now that we allow non-existing pages to trigger firewall redirections to the login page (say, example.com/registered_users_only/* redirects to the loading page, and an unauthenticated user clicks on example.com/registered_users_only/page_that_does_not_exist), we absolutely don't want to save that non-existing page as the new "TargetPath" to redirect to after a successful login, otherwise the user will see a seemingly random 404 error. We decided to extend the exception listener's setTargetPath, and defined a service that toggles whether a target path should be saved by the exception listener or not.
// Our extended ExceptionListener
protected function setTargetPath(Request $request): void
{
if ($this->targetPathSavingStatus->shouldSave()) {
parent::setTargetPath($request);
}
}
That's the purpose of the commented $this->targetPathSavingStatus->disableSaveTargetPath(); line from above: to turn the default-on status of whether to save target path on firewall exceptions to off when there's a 404 (the targetPathSavingStatus variables here point to a very simple service used only to store that piece of information).
This part of the solution is not very satisfactory. I'd like to find something better. It does seem to do the job for now though.
Of course if you have always_use_default_target_path to true, then there is no need for this particular fix.
EDIT:
To make Symfony use my versions of the Router and Exception listener, I added the following code in the process() method of Kernel.php:
public function process(ContainerBuilder $container)
{
// Use our own CatchAll router rather than the default one
$definition = $container->findDefinition('router.default');
$definition->setClass(CatchAllRouter::class);
// register the service that we use to alter the targetPath saving mechanic
$definition->addMethodCall('setTargetPathSavingStatus', [new Reference('App\Routing\TargetPathSavingStatus')]);
// Use our own ExceptionListener so that we can tell it not to use saveTargetPath
// after the CatchAll router intercepts a 404
$definition = $container->findDefinition('security.exception_listener');
$definition->setClass(FirewallExceptionListener::class);
// register the service that we use to alter the targetPath saving mechanic
$definition->addMethodCall('setTargetPathSavingStatus', [new Reference('App\Routing\TargetPathSavingStatus')]);
// ...
}
I am trying to make website where users are able to post content using an WYSIWYG editor. In my .htaccess file i have
FallbackResource index.php
which is successfully sending all requests to the index.php file. I am then using PHP to require the desired file
$path = explode('/', $_SERVER['REQUEST_URI']);
//[SCRIPT_FILENAME] => C:/xampp/htdocs/tests/index.php
if($path[2] == 'notifications'){
require_once 'notifications.php';
}
All this works fine. The problem arises when i try to redirect to an external link like google.com. The link itself gets redirected to the index page instead of being redirected.
This is the key because users can insert tags for other websites as reference in their text with the WYSIWYG editors. So my question is, Am i doing this right or do i need a different approach? and if so, which approach?. I would greatly appreciate similar approaches to those used by sites like facebook or twitter.
AS for an approach I would use a method similar to CodeIgniter or other MVC frameworks ( but simpler ).
So first you have to devise a schema, what I mean by that is something like this
www.yoursite.com/index.php/{controller}/{method}/args ...
Then you would build a router to go parse the url. I can write you one but it will take a minute.
UPDATE
You can find it on my github page here
But for refrence here is the code:
SimpleRouter.php
/**
* A simple 1 level router
*
* URL schema is http://example.com/{controller}/{method}/{args ... }
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class SimpleRouter{
/**
* should be the same as rewrite base in .htaccess
* #var string
*/
const REWRITE_BASE = '/MISC/Router/';
/**
* path to controller files
*
* #var string
*/
const CONTOLLER_PATH = __DIR__.'/Controllers/';
/**
* route a url to a controller
*/
public static function route(){
//normalize
if(self::REWRITE_BASE != '/'){
$uri = preg_replace('~^'.self::REWRITE_BASE.'~i', '',$_SERVER['REQUEST_URI']);
}
$uri = preg_replace('~^index\.php~i', '',$uri);
$uri = trim($uri,'/');
//empty url, like www.example.com
if(empty($uri)) $uri = 'home/index';
//empty method like www.example.com/home
if(!substr_count($uri, '/')) $uri .= '/index';
$arrPath = explode('/', $uri);
$contollerName = array_shift($arrPath);
$methodName = array_shift($arrPath);;
$contollerFile = self::CONTOLLER_PATH.$contollerName.'.php';
if(!file_exists($contollerFile)){
//send to error page
self::error404($uri);
return;
}
require_once $contollerFile;
if(!class_exists($contollerName)){
self::error404($uri);
return;
}
$Controller = new $contollerName();
if(!method_exists($Controller, $methodName)){
self::error404($uri);
return;
}
if(!count($arrPath)){
call_user_func([$Controller, $methodName]);
}else{
call_user_func_array([$Controller, $methodName], $arrPath);
}
}
/**
* call error 404
*
* #param string $uri
*/
protected static function error404($uri){
require_once self::CONTOLLER_PATH.'home.php';
$Controller = new home();
$Controller->error404($uri);
}
}
Default Controller home.php
/**
*
* The default controller
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class home{
public function index($arg=false){
echo "<h3>".__METHOD__."</h3>";
echo "<pre>";
print_r(func_get_args());
}
public function otherpage($arg){
echo "<h3>".__METHOD__."</h3>";
echo "<pre>";
print_r(func_get_args());
}
public function error404($uri){
header('HTTP/1.0 404 Not Found');
echo "<h3>Error 404 page {$uri} not found</h3>";
}
}
2nd Controller (example) user.php
/**
*
* An example users router
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class user{
public function index(){
echo "<h3>".__METHOD__."</h3>";
}
public function login(){
echo "<h3>".__METHOD__."</h3>";
}
}
Rewrite (remove index.php) .htaccess
RewriteEngine On
# For sub-foder installs set your RewriteBase including trailing and leading slashes
# your rewrite base will vary, possibly even being / if no sub-foder are involved
RewriteBase /MISC/Router/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]
Finally index.php
require_once __DIR__.'/SimpleRouter.php';
SimpleRouter::route();
I went with a static call to the router just to keep things simple, this way no instance is needed of the class which only has 2 methods (1 public) anyway.
I built it as a single level router, what I mean by that is that you cannot put controllers in sub-folders, such as Controllers/user/home.php instead of just Controllers/user.php. This should be fine for small to medium sized sites, but for a vary large site it may be desirable to nest the controllers. However this adds a bunch of complexity to it.
The advantage of this way, instead of individually creating the routes should be obvious. You shouldn't have to touch the code as long as the route (the url) follows the simple schema I outlined. This also allows you organize the sections of your site in their own files by functionality. For example take the user.php controller. Would let you put all that functionality in one place, such as the login, profile, logout pages etc...
If you were to add a member.php controller you can put all the stuff that you only want to show to logged in users there. And then in the __construct method of that controller you check if the current session is logged in and it covers all methods for that file.
The url schema is this http://example.com/{controller}/{method}/{args ... }. But you can also use urls like this http://example.com/index.php/{controller}/{method}/{args ... }. I'm using Mod Rewrite to allow for the removal of the index.php in the URL, so if you don't have that then you have to put index.php in the URL ( or remove it by some other means )
REWRITE_BASE On my local test server I just place everything in it's own folder as I am to lazy to setup virtual hosts. So you would want to change this constant, and the matching .htaccess, value to be whatever your sub folder is ( maybe nothing ). I just left it here for example of how to use it within a sub-folder.
Lastly, this is the C in traditional MVC architecture. You should avoid doing what is called "Business logic" in the controllers, you should also avoid outputting HTML directly from the controller (although I did this in the examples). Instead of outputting the HTML, I would suggest using a template engine like Blade or Twig there are many of these freely available. Think of a Controller like the "Glue" that joins the "Model (business logic)" to the "View (template)". Business logic would be like a User class (uppercase U like a noun, not lowercase as in our user controller) that handles the Database functionality for users. So instead of putting that code right in the Controller you would build a "User Model" and import that into the controller. This is the "Right Way" to do it, but of course it adds some complexity up front, but you will find later on the organization of it to far outweigh that in time savings.
Now some examples
404 errors (and missed routes):
http://example.com/foo routes to home::error404() and outputs <h3>Error 404 page foo/index not found</h3>
http://example.com/home/foo routes to home::error404() and outputs <h3>Error 404 page home/foo not found</h3>
Default Home page:
http://example.com/ routes to home::index()
http://example.com/index.php routes to home::index()
http://example.com/home routes to home::index()
http://example.com/home/index routes to home::index()
http://example.com/home/index/hello routes to home::index('hello')
http://example.com/home/index/hello/world routes to home::index('hello','world')
As it stands you have to put the full path in for arguments for home, the above code could be changed to account for this but it has some repercussions for missing pages. Basically any missing page would become an argument of the controller. So if you had http://example.com/user/foo it would call user::index('foo') if that was done. Of course this too can be accounted for but the complexity starts to stack up.
http://example.com/home/otherpage routes to home::otherpage() but issues a warning for missing argument.
http://example.com/home/otherpage/hello routes to home::otherpage('hello')
http://example.com/index.php/home/otherpage/hello routes to home::otherpage('hello')
If you look: home::index($arg=false) defines a default, where this method does not. So this method requires an argument. The rest of this should be pretty self explanatory.
Second Controller user example:
http://example.com/user routes to user::index()
http://example.com/user/index routes to user::index()
http://example.com/user/login routes to user::login()
The rest of this is pretty much the same, I just wanted to show how to organize the controllers by functionally for the site.
I should note that this can handle an unlimited number of arguments following the {method} in the url.
Enjoy!
My problem is concerned with:
a blog homepage called via the ArticlesController#index function
the individual blog post pages called via ArticlesController#show function, and
the comment form to be embedded into the individual blog post pages that would post to blog/{blog}/comment and would invoke the CommentsController#store method
I have the following routes of concern defined in my routes.php file:
Route::resource ('blog', 'ArticlesController');
Route::resource ('blog/{blog}/comment', 'CommentsController');
The ArticlesController is set to call the auth middleware for all functions except index() and show($id):
public function __construct()
{
$this->middleware('auth', ['except' => 'index', 'show']);
}
When I attempt to access the individual blog post pages without the comment form, it works as expected and allows me to access the homepage and individual post pages without authentication.
Whereas when I have the comment form embedded into the individual post pages, it allows me to access the homepage but demands me to authenticate before I could access the individual post pages.
Can anyone tell me why it behaves so even though my CommentsController is a separate entity and it is not invoking the auth middleware?
The except argument should be an array
public function __construct()
{
$this->middleware('auth', ['except' => ['index', 'show']]);
}
Updated per comment
If you look at the code for the middleware method if is looking for an array.
/**
* Register middleware on the controller.
*
* #param string $middleware
* #param array $options
* #return void
*/
public function middleware($middleware, array $options = [])
{
$this->middleware[$middleware] = $options;
}
As to why it worked before, who knows. I imagine that at some point along the chain of methods Laravel is converting a string to an array and that is why it worked.
You might want to consider writing a test for your controller. That way you are not dependent on any particular moment of how it is working. You know it works the way you intended and that nothing you do changes the expected result.
just started doing some symfony. The root URL i.e. /, works fine and the index/home page is returned well with all the contents. However, I just added a new route i.e. /contact, with it's required controller. When I try to get theis new contact page on a browser, I just get an empty page yet it returns a 200 response code, meaning the request was successful. Quite confusing for me. What could be the problem? Not much code for now since I got no logic but here's my controller:
class ContactController extends Controller
{
/**
* #Route("/contact")
* #Template()
*/
public function contactAction()
{
return array(
// ...
); }
}
If you load the page from app_dev.php, for example http://yourdomain.net/app_dev.php/contact, in log you can check which controller is called, and then you can debug from there the response.