How to handle exception in Router when there's no requested class? - php

I am writing my own php mvc framework (just for training). The question is how to handle exception when the requested controller doesn't exist? Should I call 404 class or create and show new View from Router? I'll be glad if you have any advices for me!
Here are my autoload.php:
function __autoload($class)
{
$filename = __DIR__ . '/' . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
if (file_exists($filename))
{
require $filename;
}
else
{
throw new \Exception('The file doesn\'t exists!');
}
}
and Route.php:
namespace App;
class Route
{
public static function start ()
{
$controller_name = 'News';
$controller_action = 'Index';
if (isset($_GET['furl']))
{
// Getting rid of spaces
$url = str_replace(' ', '', $_GET['furl']);
if (substr($url, -1) == '/')
{
$url = substr($url, 0, count($url) - 2);
}
$arr = explode('/', $url);
foreach($arr as &$value)
{
$value = strtolower($value);
$value = ucfirst($value);
}
$controller_action = $arr[count($arr) - 1];
unset($arr[count($arr) - 1]);
$controller_name = implode('\\', $arr);
}
$controller_name = '\App\Controllers\\' . $controller_name;
try
{
$controller = new $controller_name();
}
catch (\Exception $e)
{
//HELP ME PLS!
}
$controller->action($controller_action);
}
}

No matter how many web frameworks, routers, autoloaders, etc are there already: keep doing what you think it's right for you and suitable to your momentarily understanding level, in order to LEARN. Actually, by confronting yourself with problems arised along the process of implementing yourself different parts of your application, you will not only gain the opportunity to learn and discover new things, but also to learn how and what to study in the already existing frameworks' design.
Study the PHP Standard Recommendations (the ones marked as "accepted"). Especially PSR-1,2,4,7. They are used by many frameworks and PHP projects. Read FAQs to find out more about the project itself.
Autoloader:
The PSR-4 provides a link with examples at the document end.
#mike suggested, that you should use the Composer autoloader. I agree with him and I strongly recommend it to you too. BUT I suggest you to do this only after you correctly implement and make use of your own autoloader (PSR-4 conform). Why? You definitely need to learn how the autoloading process works. And in some future situations you will still need your own autoloader implementation, even after Composer is installed and running.
Also be aware that you must not raise any exceptions from autoloader itself!
Router:
Btw, your class should be called "Router".
The router should not be responsible for validating the controller class/file and the action, nor for calling the action. These tasks are part of the "front controller" responsibilities. Your router should just return the components resulted after parsing, e.g. "exploding" the request URI ($_GET['furl']), in some form (as a Route object (with them as properties), as array, etc). These components are the controller name, the action name, the action parameters list (NB: the action parameters are not the query string parameters). The front controller uses them further to validate/access the controller class/file and its action and to call the action.
But please note that a router works actually in other way. In short: it matches (e.g. compares) the request method (GET, POST, etc) and the request URI against an existing (e.g. predefined by you) list of route definitions. A route definition contains the infos related to a specific controller, action, etc. If the HTTP method and the request URI "correspond" to one of the route definitions, then the router returns the matched definition's components to the front controller (in some form: as object, as array, etc).
For more details describing this principle see:
How to load classes based on pretty URLs in MVC-like page?
FastRoute
Aura.Router
Front controller:
It can be a class, but it can also be just vanilla code in the entry point of your app (index.php, bootstrap.php, etc). In the latter case, the front controller code should reside in a file outside of the document root of the app. For example in a bootstrap.php file, which is to be just included in index.php - whereas index.php resides inside the document root.
"controller/action not found" specific handling:
If a controller, or an action is not found/valid, then call a predefined action (for example displayError) of a predefined Error controller, which informs the user that, for a specific part of his request (actually of his provided request URI), no resource was found.
For example, consider that the user provided the request URI www.example.com/books/show/12. Conform to your app workflow the controller is Book, the action (e.g. the controller's method) is show and the action parameter is 12 (the value is passed as argument to the show method and defined as $bookId parameter in it). But, if the controller class is not defined, or no controller file exists, then the front controller should call the action displayError of Error controller, which should display a message like No resource found for your 'book' request. A similar info should be displayed when the show method is not yet defined in the Book controller.
Note that, if the Error controller or its action is not found/valid, then the PHP engine raises a corresponding error/exception. If you follow the next links I provided, you'll end up implementing three custom error/exception handling functions (referenced by set_error_handler, set_exception_handler and register_shutdown_function, respectively). They will catch and handle the described situation properly.
To read: Manage the errors of a framework
General error/exception handling in MVC:
Here are some good resources:
Again: Manage the errors of a framework
Error logging, in a smooth way
Error reporting basics
The (im)proper use of try..catch
Other MVC related resources:
Build a PHP MVC Application (Just for the start...)
Dependency Injection and Dependency Inversion in PHP
MVC for advanced PHP developers (A further list of resources)
Tom Butler's Programming Blog. MVC, PHP, Best practices
Clean, high quality code
P.S: Avoid the use of statics, globals, singletons. Why? Read here and here, for example.
Good luck.

Related

Loading Modules Dynamically in Zend Framework 2

I have asked this question yesterday as well, but this one includes code.
Issue
My application have multiple modules and 2 types of user accounts, Some modules are loaded always which are present in application.config.php some of them are conditional i.e. some are loaded for user type A and some for user type B
After going through documentations and questions on Stack Overflow, I understand some of ModuleManager functionalities and started implementing the logic that I though might work.
Some how I figured out a way to load the modules that are not present in application.config.php [SUCCESS] but their configuration is not working [THE ISSUE] i.e. if in onBootstrap method I get the ModuleManager service and do getLoadedModules() I get the list of all the modules correctly loaded. Afterwards if I try to get some service from that dynamically loaded module, it throws exception.
Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for jobs_mapper
Please note that, the factories and all other stuff are perfectly fine because if I load the module from application.config.php it works fine
Similarly when I try to access any route from the dynamically loaded module it throws 404 Not Found which made it clear that the configuration from module.config.php of these modules are not loading even though the module is loaded by ModuleManager.
Code
In Module.php of my Application module I implemented InitProviderInterface and added a method init(ModuleManager $moduleManager) where I catch the moduleManager loadModules.post event trigger and load modules
public function init(\Zend\ModuleManager\ModuleManagerInterface $moduleManager)
{
$eventManager = $moduleManager->getEventManager();
$eventManager->attach(\Zend\ModuleManager\ModuleEvent::EVENT_LOAD_MODULES_POST, [$this, 'onLoadModulesPost']);
}
Then in the same class I delcare the method onLoadModulesPost and start loading my dynamic modules
public function onLoadModulesPost(\Zend\ModuleManager\ModuleEvent $event)
{
/* #var $serviceManager \Zend\ServiceManager\ServiceManager */
$serviceManager = $event->getParam('ServiceManager');
$configListener = $event->getConfigListener();
$authentication = $serviceManager->get('zfcuser_auth_service');
if ($authentication->getIdentity())
{
$moduleManager = $event->getTarget();
...
...
$loadedModules = $moduleManager->getModules();
$configListener = $event->getConfigListener();
$configuration = $configListener->getMergedConfig(false);
$modules = $modulesMapper->findAll(['is_agency' => 1, 'is_active' => 1]);
foreach ($modules as $module)
{
if (!array_key_exists($module['module_name'], $loadedModules))
{
$loadedModule = $moduleManager->loadModule($module['module_name']);
//Add modules to the modules array from ModuleManager.php
$loadedModules[] = $module['module_name'];
//Get the loaded module
$module = $moduleManager->getModule($module['module_name']);
//If module is loaded succesfully, merge the configs
if (($loadedModule instanceof ConfigProviderInterface) || (is_callable([$loadedModule, 'getConfig'])))
{
$moduleConfig = $module->getConfig();
$configuration = ArrayUtils::merge($configuration, $moduleConfig);
}
}
}
$moduleManager->setModules($loadedModules);
$configListener->setMergedConfig($configuration);
$event->setConfigListener($configListener);
}
}
Questions
Is it possible to achieve what I am trying ?
If so, what is the best way ?
What am I missing in my code ?
I think there is some fundamental mistake in what you are trying to do here: you are trying to load modules based on merged configuration, and therefore creating a cyclic dependency between modules and merged configuration.
I would advise against this.
Instead, if you have logic that defines which part of an application is to be loaded, put it in config/application.config.php, which is responsible for retrieving the list of modules.
At this stage though, it is too early to depend on any service, as service definition depends on the merged configuration too.
Another thing to clarify is that you are trying to take these decisions depending on whether the authenticated user (request information, rather than environment information) matches a certain criteria, and then modifying the entire application based on that.
Don't do that: instead, move the decision into the component that is to be enabled/disabled conditionally, by putting a guard in front of it.
What you're asking can be done, but that doesn't mean you should.
Suggesting an appropriate solution without knowing the complexity of the application you're building is difficult.
Using guards will certainly help decouple your code, however using it alone doesn't address scalability and maintainability, if that's a concern?
I'd suggest using stateless token-based authentication. Instead of maintaining the validation logic in every application, write the validation logic at one common place so that every request can make use of that logic irrespective of application. Choosing a reverse proxy server (Nginx) to maintain the validation logic (with the help of Lua) gives you the flexibility to develop your application in any language.
More to the point, validating the credentials at the load balancer level essentially eliminates the need for the session state, you can have many separate servers, running on multiple platforms and domains, reusing the same token for authenticating the user.
Identifying the user, account type and loading different modules then becomes a trivial task. By simply passing the token information via an environment variable, it can be read within your config/application.config.php file, without needing to access the database, cache or other services beforehand.

Testing Laravel Service Providers

I'm (we're) creating a package that acts as a core component for our future CMS and of course that package needs some unit tests.
When the package registeres, the first thing it does is set the back/frontend context like this:
class FoundationServiceProvider extends ServiceProvider
{
// ... stuff ...
public function register()
{
// Switch the context.
// Url's containing '/admin' will get the backend context
// all other urls will get the frontend context.
$this->app['build.context'] = request()->segment(1) === 'admin'
? Context::BACKEND
: Context::FRONTEND;
}
}
So when I visit the /admin url, the app('build.context') variable will be set to backend otherwise it will be set to `frontend.
To test this I've created the following test:
class ServiceProviderTest extends \TestCase
{
public function test_that_we_get_the_backend_context()
{
$this->visit('admin');
$this->assertEquals(Context::BACKEND, app('build.context'));
}
}
When I'm running the code in the browser (navigating to /admin) the context will get picked up and calling app('build.context') will return backend, but when running this test, I always get 'frontend'.
Is there something I did not notice or some incorrect code while using phpunit?
Thanks in advance
Well, this is a tricky situation. As I understand it, laravel initiates two instances of the framework when running tests - one that is running the tests and another that is being manipulated through instructions. You can see it in tests/TestCase.php file.
So in your case you are manipulating one instance, but checking the context of another (the one that did not visit /admin and is just running the tests). I don't know if there's a way to access the manipulated instance directly - there's nothing helpful in documentation on this issue.
One workaround would be to create a route just for testing purposes, something like /admin/test_context, which would output the current context, and the check it with
$this->visit('admin/test_context')->see(Context::BACKEND);
Not too elegant, but that should work. Otherwise, look around in laravel, maybe you will find some undocumented feature.

Proceedural Code in Silex or maybe other Microframeworks

I'm taking an application I wrote, a form, and rewriting it in Silex. It was, for various reasons, written like:
page1.php
page1_process.php
page2.php
page2_process.php
page3.php
page3_process.php
Where pageX.php is an HTML form with little PHP code, that passes data to the process script. The process script stores data to a Form object, and serializes the object in the Session.
Currently, I have the pageX.php pages as TWIG template files and they work, but I don't know how to incorporate the logic of the procedural process scripts.
I'm sure I don't copy the actual logic to the controllers.
If I follow your requirements, you just need the same route twice: one for get (displaying the form) and one for post to handle it. In the post controller you just need to include your pageX_process.php and you should be ready to go.
Translated in Silex code it should be something like:
<?php
/**
* if you're using silex-skeleton
* from: https://packagist.org/packages/fabpot/silex-skeleton)
* this file should be src/controllers
*/
// standard setup, like error handling and other route declarations
$app->get('/page1', function() use ($app) {
// you're currently using this (somehow)
$params = []; //set up your template parameters here
return $app['twig']->render('page1.twig', $params);
});
$app->post('/page1_proccess', function() use($app) {
ob_start();
require PATH_TO_LEGACY_FILES_DIR . '/page1_process.php';
return ob_get_clean();
});
From now on, and if you want / find it adequate, you can start to refactor your pageX_process.php pages in a more OOP / Silex way, but you have a starting point with this application architecture.
NOTICE:
you should move your php files away from the web directory (for example legacy/ in the root of your project)
you must point your form handling script (the action parameter) to the new route (you can make it to work using the old route also but requires some little more effort)

Yii: custom error handling forwarding

I'm looking for a way to forward the error handling in my Yii 1.1.14 app. The scenario comes as follows:
Assuming I have two modules: ClientModule at /client/, AdminModule at /admin/. If an url is resolved to belong to any controller in that module, the controller is loaded, and the errorHandler is reassigned to a module-level error handler like this:
public function beforeControllerAction($controller, $action)
{
Yii::app()->errorHandler->errorAction='admin/error';
return parent::beforeControllerAction($controller, $action);
}
In this way, an action exists at 'admin/admin/error' (which in turn is manually specified to be resolved to 'admin/error'), which will handle the error with a boilerplate like:
public function actionError() {
if($error=Yii::app()->errorHandler->error)
{
if(Yii::app()->request->isAjaxRequest)
echo $error['message'];
else
$this->render('application.views.error.index', $error);
}
}
Meanwhile, the analogous code (for both functions) exist in the other module (There's also a ClientModule and a ClientController with an actionError mapped automatically to 'client/client/error', remapped to 'client/error').
I have no trouble at all with these code chunks. My issue starts now:
If I input an url which cannot be resolved, even when the prefix is a module prefix (e.g. 'client/invalid/url' or 'client/client/invalid'), the controller will not be created (since it does not exist - or, as in the second example, the controller exists but not the action), and so beforeController will not be called, and so the custom error handler (in this example: the corresponding to ClientModule which sets $aClientController->actionError) will not be assigned. Result: the default ErrorController handling the unresolved url error.
So, questions:
Is there any way I could map an unresolved url (404) error to certain module, depending on the prefix? (it is safe, in my case, to assume prefixes, since I have not set any module as default).
Alternatively: is there a way to, being in the ErrorController->actionIndex(), forward the error handling to one of those controllers (i.e. moving to admin/admin/error and client/client/error while keeping the Yii::app()->errorHandler->error state)?
Edit - Footnote: Why should I use another controller if the error handling code is the same? Because I have additional data provided by those controllers which is used in the layout (e.g. menu, head menu).
Found the answer, again (my juniority has no limits - and the method name I needed had the exact name: forward!!).
Using the CController->forward('module/controller/action') inside the ErrorController->actionError method I could dispatch everywhere. I also changed another business logic condition and have not the custom error handler assignment in the modules constructors anymore, but I still use those controllers as forward targets of 'error/index' depending on the logged user type (i.e. an error occurred and the user was a client => forward to client/client/error, while being admin led to admin/admin/error, and being no logged user performed the error handling with error/index as usual).
public function actionIndex() {
if (Yii::app()->user->getState('client'))
{
$this->forward('client/client/error');
}
if (Yii::app()->user->getState('staff'))
{
$this->forward('admin/admin/error');
}
//usual boilerplate here
if($error=Yii::app()->errorHandler->error)
{
if(Yii::app()->request->isAjaxRequest)
echo $error['message'];
else
$this->render('index', $error);
}
}

The best way to deal with 404 errors in MVC?

So I wrote my PHP MVC framework, and have Dispatcher class that can instantiate appropriate Controller class and call defined method passing arguments.
Now inside Dispatcher I check if this Controller exists and if method exists, what should I do if controller or method does not exist?
At the moment I just return HTTP Object that prints 404 - Page not found.
But there is no way for me to customize this message from inside application, and I want to provide users a way to customize 404 messages without editing dispatcher.
Is a good way to go to always have Error controller that would get instantiated when there is a error, and that would load lets say Error404.html view file?
So users would be able to customize this view file to fit their application design.
Is there any other way to achive this? And what would be the best way to return error from dispatcher, and let "users" or developers that are working on that MVC to easily customize 404 and other messages?
Thanks!
Since i do not know your API, I am going to guess. Lets assume that you have a bootstrap stage in your application, when the dispatcher is actually used. Something like:
$dispatcher->dispatch( $request );
Then for handling request, that try to access non-existent controllers or methods within those controllers, you can do something like this:
try
{
$dispatcher->dispatch( $request );
}
catch ( ClassNotFoundException $e )
{
$dispatcher->dispatch( new Request('/error/404/controller'));
}
catch ( MethodNotFoundException $e )
{
$dispatcher->dispatch( new Request('/error/404/method'));
}
The ClassNotFoundException can be thrown by your classloader, while dispatcher itself would always be responsible for throwing the MethodNotFoundException.
You can check, whether controller has a particular method, with method_exists(), before executing it in your dispatcher.
P.S. in my humble opinion, the Dispatcher conept is better suited for event driven architectures and not for MVC-inspired patterns in web applications.
I would propose you have an error controller that takes in an error code (number or string) as an argument. This allows you to gracefully handle various kinds of errors and be able to provide a stack trace if necessary. You can even utilize this work for 500 errors.
My answer comes with the assumption that a controller can return various actions and each action can have it's own template.
Symfony also seems to handle errors in a similar fashion. They have a separate module and action for each error.
sfContext::getInstance()->getController()->forward(sfConfig::get('sf_error_404_m‌​odule'), sfConfig::get('sf_error_404_action'));

Categories