0

I have an MVC structure in my php.

I use autoload to initialize the correct controller.

Here is a very dumbed down version of my index.php:

<?php
spl_autoload_extensions('.class.php');
spl_autoload_register();

$controller = strtolower($_GET["controller"]);
$action = strtolower($_GET["action"]);

$obj = new $controller();
$obj->{$action}();

So let's say the user loads www.example.com/Page/View. Apache will rewrite it to www.example.com?controller=Page&action=View. Then when php calls $obj = new $controller();, it will try to find page.class.php in the same directory as the file (obviously in my application the file structure isn't so trivial, with namespaces, etc...), and then load the Page class inside and execute Page->View();.

Now, say the user makes a typo and tries to load www.example.com/Pagr/View. Ideally, he should get a 404 header response. But with the current implementation, php will just throw a Fatal error when it fails to autoload pagr.class.php.

How can I prevent this error from happening? I've done some research, and I can't figure out a way to check if the class can be autoloaded or not prior to the new call.

Nikita240
  • 1,377
  • 2
  • 14
  • 29

4 Answers4

4

Use class_exists() to check if the class exists before you try to load it.

However, here's how I'd do it:

class ClassNotFoundException extends Exception {}

function autoloadClass($class) {
  $file = $class . '.class.php';

  if(!file_exists($file)) {
    throw new ClassNotFoundException($class);
  }

  require($file);
}

spl_autoload_register('autoloadClass');

try {
  $obj = new $controller();
}
catch(ClassNotFoundException $e) {
  // call 404 page
}
halloei
  • 1,892
  • 1
  • 25
  • 45
  • `class_exists()` checks if a class is already loaded. It doesn't check whether it can be autoloaded. – Nikita240 Mar 24 '15 at 08:19
  • can't you check file existance? – B-and-P Mar 24 '15 at 08:20
  • @halloei Your implementation is the most obvious one, however, people report that apparently php ignores exceptions thrown inside the `__autoload` function and throws the `Fatal error` anyway. See [this](http://stackoverflow.com/a/1589227/2449639). – Nikita240 Mar 24 '15 at 08:40
  • I'm currently thinking that the easiest way is to probably just try to check with `class_exists` and catch the exception it throws. I'm going to try it. – Nikita240 Mar 24 '15 at 08:45
  • 1
    _Note: Prior to 5.3.0, exceptions thrown in the __autoload function could not be caught in the catch block and would result in a fatal error. From 5.3.0+ exceptions thrown in the __autoload function can be caught in the catch block, with 1 provision. If throwing a custom exception, then the custom exception class must be available. The __autoload function may be used recursively to autoload the custom exception class._ – halloei Mar 24 '15 at 08:46
  • Oooh I didn't see that. That changes everything. – Nikita240 Mar 24 '15 at 08:50
  • What you suggest here is not a good idea. Throwing exceptions or raising errors in the autoloader prevents other autoloaders from doing their job. Use class_exists() instead. – donquixote Sep 17 '15 at 04:30
1

Unless what you absolutely need is a way to check if the class can be autoloaded. Otherwise, if you need to check for the existence of the controller file before instantiating the object, you can try:

if(file_exists(strtolower($controller).'.class.php') && class_exists ($controller, false))
{
   $obj = new $controller();
}

The second argument passed to class_exists() ensures that it does not try to autoload the class. I don't know if this helps you in any way.

NaijaProgrammer
  • 2,892
  • 2
  • 24
  • 33
  • Well, it would have to be a bit more complicated since for a proper implementation you'd have to calculate the include path and namespacing, but yeah you can check if the file exists. But the problem is it will still throw an error if the class cannot be found within the file, which is a situation in which you would still want to return a 404. – Nikita240 Mar 24 '15 at 08:27
  • 1
    Your best bet will be to combine the check above with what @halloei suggests. I'll update my answer to reflect what I mean. – NaijaProgrammer Mar 24 '15 at 08:32
1

@halloei is correct, but it requires a comment to explain why it is correct. class_exists is yet another funny PHP function that tries to autoload class in case it doesn't exist. This is quite counterintuitive, but this is how it works (this behavior can be turned off by second argument). So, class_exists will tell you not if class exists, but if it exists or in reach of registered autoloaders.

Etki
  • 2,042
  • 2
  • 17
  • 40
  • Hmm. When I try to run `class_exists` on a class that cannot be autoloaded, it throws a `LogicException`. [This is apparently a bug in php](https://bugs.php.net/bug.php?id=52339). – Nikita240 Mar 24 '15 at 08:34
  • @Nikita240 i guess path checking / exception catching is your only option here. But i strongly suggest you to avoid this pattern and rewrite architecture simply because anyone can call `__construct()` on any class or even do something worse. – Etki Mar 24 '15 at 08:37
  • Can you elaborate on the architecture issue? – Nikita240 Mar 24 '15 at 08:42
  • @Nikita240 i'd implement a simple router, just a map of a regexp patterns into specific controller actions (e.g. `['user/(\d+)' => 'User/Display']`) and checked request uri against those regexps. As far as i can understand, this is not a big project, so implementing it so plainly won't be a trouble (i'd taken a framework for any project, but it's not my decision here), and that would restrict what can be executed. 'Nothing matched' case should simply render a 404 page. – Etki Mar 24 '15 at 08:48
0

Here is one way to do it.

class_exists() supposedly does attempt to autoload a class if it isn't already defined. However, there is a bug currently in php which makes class_exists() throw a LogicException if the autoloader fails. But we can simply catch this exception until the bug is fixed!

<?php
spl_autoload_extensions('.class.php');
spl_autoload_register();

$controller = strtolower($_GET["controller"]);
$action = strtolower($_GET["action"]);

try {
    $class_exists = class_exists($controller);
}
catch(LogicException $e) {
    $class_exists = false;
}
finally {
    if(!$class_exists) {
        http_response_code(404);
        exit();
    }
}

$obj = new $controller();
$obj->{$action}();
Nikita240
  • 1,377
  • 2
  • 14
  • 29