Tests fail with "Class 'Browser" could not be found - php

I have a test folder structure like:
.
├── test
│ └── e2e
│ ├── pages
│ │ └── LoginPage.php
│ └── webdriver
│ └── Browser.php
└── vendor
When I run
$ vendor\bin\phpunit --bootstrap vendor/autoload.php test\e2e\HomePage.php
the test fails with
Fatal error: Uncaught PHPUnit\Runner\Exception: Class 'Browser' could not be found in 'C:\Users\rburton\code\MyAgsourceAPI\test\e2e\HomePage.php'. in C:\Users\rburton\code\MyAgsourceAPI\vendor\phpunit\phpunit\src\Runner\StandardTestSuiteLoader.php on line 101
PHPUnit\Runner\Exception: Class 'Browser' could not be found in 'C:\Users\rburton\code\MyAgsourceAPI\test\e2e\HomePage.php'. in C:\Users\rburton\code\MyAgsourceAPI\vendor\phpunit\phpunit\src\Runner\StandardTestSuiteLoader.php on line 101
Call Stack:
0.1973 357496 1. {main}() C:\Users\rburton\code\MyAgsourceAPI\vendor\phpunit\phpunit\phpunit:0
0.2024 746904 2. PHPUnit\TextUI\Command::main(???) C:\Users\rburton\code\MyAgsourceAPI\vendor\phpunit\phpunit\phpunit:53
0.2024 750064 3. PHPUnit\TextUI\Command->run(array(4), true) C:\Users\rburton\code\MyAgsourceAPI\vendor\phpunit\phpunit\src\TextUI\Command.php:141
0.2077 1007320 4. PHPUnit\TextUI\TestRunner->getTest(string(17), string(57), array(2)) C:\Users\rburton\code\MyAgsourceAPI\vendor\phpunit\phpunit\src\TextUI\Command.php:162
0.2078 1007320 5. PHPUnit\TextUI\TestRunner->loadSuiteClass(string(17), string(57)) C:\Users\rburton\code\MyAgsourceAPI\vendor\phpunit\phpunit\src\Runner\BaseTestRunner.php:73
0.2084 1026680 6. PHPUnit\Runner\StandardTestSuiteLoader->load(string(7), string(57)) C:\Users\rburton\code\MyAgsourceAPI\vendor\phpunit\phpunit\src\Runner\BaseTestRunner.php:130
The only place that I am instantiating the Browser class is in the setup for the HomePage test class:
protected function setUp()
{
$this->browserDriver = new Browser();
$this->browserDriver->start('https://192.168.3.69');
}
The thing that puzzles me is that the error indicates that the StandardTestSuiteLoader could not find the Browser class rather than in the test class where the class is instantiated.
Does anyone have some ideas why the auxillary class cannot be found?

I looks like selenium in the PHPUnit library is cannot be bundled as a framework. I have since moved on to the selenium libraries for C# and Java.

Related

More than one application per project repository with Symfony 4

I have three old applications (running on Symfony 2) where each one has been developed in separated git repositories and configured in their respective vhosts:
company.com Company website.
admin.company.com Website administration.
api.company.com API company service.
Even though, they share the same database. So we're decided (the Company) unify all of them in one application with Symfony 4 structure & approach, mainly to remove a big quantity of duplicated data and to improve its maintenance.
Right now, I'm integrating all in one application/repository as was planned, but I'm starting to deal with some performance & structure issues:
As I've just one entry point index.php I did two routes prefixes to be able to access for company.com/admin/ and company.com/api/ sub app, so all routes are loaded each time :(
All bundles and configuration is loaded and processed needlessly for each request. For example: when I access the API path the SonataAdminBundle is loaded too :(
The cache clear command takes a long time to complete.
The tests are breaking down and now takes a long time to complete too.
I'd like to keep the early vhost and load just the needed bundles and configuration per domains:
company.com Loads bundles, routes and configuration for a company website only (SwiftmailerBundle, ...)
admin.company.com Loads bundles, routes and configuration only for website administration (SecurityBundle, SonataAdminBundle, ...)
api.company.com Loads just the bundles, routes and configuration to provide a fast API company service (SecurityBundle, FOSRestBundle, NelmioApiDocBundle, ...)
This is what I'm doing so far:
// public/index.php
// ...
$request = Request::createFromGlobals();
$kernel = new Kernel(getenv('APP_ENV'), getenv('APP_DEBUG'));
// new method implemented in my src/kernel.php
$kernel->setHost($request->server->get('HTTP_HOST'));
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
I've check the current host prefix in Kernel::registerBundles() method and I loaded the needed bundles only, but still I've problems with bin/console file (it doesn't work as HTTP_HOST variable is not defined for CLI) I'd like to clear the cache for each "sub-app" and so on.
I have been doing some research on this topic but so far I couldn't find anything helpful for my scenario (Symfony 4).
Is possible to have many applications under one project repository running independently (like individual apps) but sharing some configuration? What is the best approach to achieve it?
Thanks in advance.
Likely the multiple kernels approach could be a good option to solve this kind of project, but thinking now in Symfony 4 approach with environment variables, structure and kernel implementation, it could be improved.
Name-based Virtual Kernel
The term "Virtual Kernel" refers to the practice of running more than one application (such as api.example.com and admin.example.com) on a single project repository. Virtual kernels are "name-based", meaning that you have multiple kernel names running on each application. The fact that they are running on the same physical project repository is not apparent to the end user.
In short, each kernel name corresponds to one application.
Application-based Configuration
First, you'll need replicate the structure of one application for config, src, var directories and leave the root structure for shared bundles and configuration. It should look like this:
├── config/
│ ├── admin/
│ │ ├── packages/
│ │ ├── bundles.php
│ │ ├── routes.yaml
│ │ ├── security.yaml
│ │ └── services.yaml
│ ├── api/
│ ├── site/
│ ├── packages/
│ ├── bundles.php
├── src/
│ ├── Admin/
│ ├── Api/
│ ├── Site/
│ └── VirtualKernel.php
├── var/
│ ├── cache/
│ │ ├── admin/
│ │ │ └── dev/
│ │ │ └── prod/
│ │ ├── api/
│ │ └── site/
│ └── log/
Next, making use of the Kernel::$name property you can stand out the application to run with dedicated project files (var/cache/<name>/<env>/*):
<name><Env>DebugProjectContainer*
<name><Env>DebugProjectContainerUrlGenerator*
<name><Env>DebugProjectContainerUrlMatcher*
This will be the key of the performance as each application has by definition its own DI container, routes and configuration files. Here is a complete sample of the VirtualKernel class that supports the previous structure:
src/VirtualKernel.php
// WITHOUT NAMESPACE!
use Symfony\Component\HttpKernel\Kernel;
class VirtualKernel extends Kernel
{
use MicroKernelTrait;
private const CONFIG_EXTS = '.{php,xml,yaml,yml}';
public function __construct($environment, $debug, $name)
{
$this->name = $name;
parent::__construct($environment, $debug);
}
public function getCacheDir(): string
{
return $this->getProjectDir().'/var/cache/'.$this->name.'/'.$this->environment;
}
public function getLogDir(): string
{
return $this->getProjectDir().'/var/log/'.$this->name;
}
public function serialize()
{
return serialize(array($this->environment, $this->debug, $this->name));
}
public function unserialize($data)
{
[$environment, $debug, $name] = unserialize($data, array('allowed_classes' => false));
$this->__construct($environment, $debug, $name);
}
public function registerBundles(): iterable
{
$commonBundles = require $this->getProjectDir().'/config/bundles.php';
$kernelBundles = require $this->getProjectDir().'/config/'.$this->name.'/bundles.php';
foreach (array_merge($commonBundles, $kernelBundles) as $class => $envs) {
if (isset($envs['all']) || isset($envs[$this->environment])) {
yield new $class();
}
}
}
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
$container->setParameter('container.dumper.inline_class_loader', true);
$this->doConfigureContainer($container, $loader);
$this->doConfigureContainer($container, $loader, $this->name);
}
protected function configureRoutes(RouteCollectionBuilder $routes): void
{
$this->doConfigureRoutes($routes);
$this->doConfigureRoutes($routes, $this->name);
}
private function doConfigureContainer(ContainerBuilder $container, LoaderInterface $loader, string $name = null): void
{
$confDir = $this->getProjectDir().'/config/'.$name;
if (is_dir($confDir.'/packages/')) {
$loader->load($confDir.'/packages/*'.self::CONFIG_EXTS, 'glob');
}
if (is_dir($confDir.'/packages/'.$this->environment)) {
$loader->load($confDir.'/packages/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob');
}
$loader->load($confDir.'/services'.self::CONFIG_EXTS, 'glob');
if (is_dir($confDir.'/'.$this->environment)) {
$loader->load($confDir.'/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob');
}
}
private function doConfigureRoutes(RouteCollectionBuilder $routes, string $name = null): void
{
$confDir = $this->getProjectDir().'/config/'.$name;
if (is_dir($confDir.'/routes/')) {
$routes->import($confDir.'/routes/*'.self::CONFIG_EXTS, '/', 'glob');
}
if (is_dir($confDir.'/routes/'.$this->environment)) {
$routes->import($confDir.'/routes/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob');
}
$routes->import($confDir.'/routes'.self::CONFIG_EXTS, '/', 'glob');
}
}
Now your \VirtualKernel class requires an extra argument (name) that defines the application to load. In order for the autoloader to find your new \VirtualKernel class, make sure add it to composer.json autoload section:
"autoload": {
"classmap": [
"src/VirtualKernel.php"
],
"psr-4": {
"Admin\\": "src/Admin/",
"Api\\": "src/Api/",
"Site\\": "src/Site/"
}
},
Then, run composer dump-autoload to dump the new autoload config.
Keeping one entry point for all applications
├── public/
│ └── index.php
Following the same filosofy of Symfony 4, whereas environment variables decides which development environment and debug mode should be used to run your application, you could add a new APP_NAME environment variable to set the application to execute:
public/index.php
// ...
$kernel = new \VirtualKernel(getenv('APP_ENV'), getenv('APP_DEBUG'), getenv('APP_NAME'));
// ...
For now, you can play with it by using PHP's built-in Web server, prefixing the new application environment variable:
$ APP_NAME=site php -S 127.0.0.1:8000 -t public
$ APP_NAME=admin php -S 127.0.0.1:8001 -t public
$ APP_NAME=api php -S 127.0.0.1:8002 -t public
Executing commands per application
├── bin/
│ └── console.php
Add a new console option --kernel to be able to run commands from different applications:
bin/console
// ...
$name = $input->getParameterOption(['--kernel', '-k'], getenv('APP_NAME') ?: 'site');
//...
$kernel = new \VirtualKernel($env, $debug, $name);
$application = new Application($kernel);
$application
->getDefinition()
->addOption(new InputOption('--kernel', '-k', InputOption::VALUE_REQUIRED, 'The kernel name', $kernel->getName()))
;
$application->run($input);
Later, use this option to run any command different to default one (site).
$ bin/console about -k=api
Or if you prefer, use environment variables:
$ export APP_NAME=api
$ bin/console about # api application
$ bin/console debug:router # api application
$
$ APP_NAME=admin bin/console debug:router # admin application
Also you can configure the default APP_NAME environment variable in the .env file.
Running tests per application
├── tests/
│ ├── Admin/
│ │ └── AdminWebTestCase.php
│ ├── Api/
│ ├── Site/
The tests directory is pretty similar to the src directory, just update the composer.json to map each directory tests/<Name>/ with its PSR-4 namespace:
"autoload-dev": {
"psr-4": {
"Admin\\Tests\\": "tests/Admin/",
"Api\\Tests\\": "tests/Api/",
"Site\\Tests\\": "tests/Site/"
}
},
Again, run composer dump-autoload to re-generate the autoload config.
Here, you might need create a <Name>WebTestCase class per application in order to execute all tests together:
test/Admin/AdminWebTestCase
namespace Admin\Tests;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
abstract class AdminWebTestCase extends WebTestCase
{
protected static function createKernel(array $options = array())
{
return new \VirtualKernel(
isset($options['environment']) ? $options['environment'] : 'test',
isset($options['debug']) ? $options['debug'] : true,
'admin'
);
}
}
Later, extends from AdminWebTestCase to test admin.company.com application (Do the same for another ones).
Production and vhosts
Set the environment variable APP_NAME for each vhost config in your production server and development machine:
<VirtualHost company.com:80>
SetEnv APP_NAME site
# ...
</VirtualHost>
<VirtualHost admin.company.com:80>
SetEnv APP_NAME admin
# ...
</VirtualHost>
<VirtualHost api.company.com:80>
SetEnv APP_NAME api
# ...
</VirtualHost>
Adding more applications to the project
With three simple steps you should be able to add new vKernel/applications to the current project:
Add to config, src and tests directories a new folder with the <name> of the application and its content.
Add to config/<name>/ dir at least the bundles.php file.
Add to composer.json autoload/autoload-dev sections the new PSR-4 namespaces for src/<Name>/ and tests/<Name> directories and update the autoload config file.
Check the new application running bin/console about -k=<name>.
Final directory structure:
├── bin/
│ └── console.php
├── config/
│ ├── admin/
│ │ ├── packages/
│ │ ├── bundles.php
│ │ ├── routes.yaml
│ │ ├── security.yaml
│ │ └── services.yaml
│ ├── api/
│ ├── site/
│ ├── packages/
│ ├── bundles.php
├── public/
│ └── index.php
├── src/
│ ├── Admin/
│ ├── Api/
│ ├── Site/
│ └── VirtualKernel.php
├── tests/
│ ├── Admin/
│ │ └── AdminWebTestCase.php
│ ├── Api/
│ ├── Site/
├── var/
│ ├── cache/
│ │ ├── admin/
│ │ │ └── dev/
│ │ │ └── prod/
│ │ ├── api/
│ │ └── site/
│ └── log/
├── .env
├── composer.json
Unlike multiple kernel files approach, this version reduces a lot of code duplication and files; just one kernel, index.php and console for all applications, thanks to environment variables and virtual kernel class.
Example based-on Symfony 4 skeleton: https://github.com/yceruto/symfony-skeleton-vkernel
Inspired in https://symfony.com/doc/current/configuration/multiple_kernels.html
You can create new environments like: admin, website, api. Then by provide environment variable SYMFONY_ENV by apache/nginx you will be able to run dedicated application and still use sub domains company.com, admin.company.com, api.company.com. Also you will be able to easily load only required routing.
Depends from how many application you want to create based on this approach you can add conditions to load specified bundles by project in AppKernel class or create separate classes for each project.
You should also read this article https://jolicode.com/blog/multiple-applications-with-symfony2
Also when you want to run Behat testing, you should run it with this command:
for windows:
set APP_NAME=web&& vendor\bin\behat
for linux:
export APP_NAME='web' && vendor\bin\behat
where "web" is your kernel name you want to run.
The KernelInterface::getName() method and the kernel.name parameter have been deprecated. There's no alternative to them because this is a concept that no longer makes sense in Symfony applications.
If you need a distinctive ID for the kernel of the application, you can use the KernelInterface::getContainerClass() method and the kernel.container_class parameter.
Similarly, the getRootDir() method and the kernel.root_dir parameter have been deprecated too. The alternative is to use the getProjectdir() and kernel.project_dir method introduced in Symfony 3.3
See https://symfony.com/blog/new-in-symfony-4-2-important-deprecations#deprecated-the-kernel-name-and-the-root-dir

How to properly call controller/action in Zend Framework 1?

I have the following directory structure in a Zend Framework 1 application:
application/
├── controllers/
│   └── admin/
│   └── TaxRateController.php
│   └── MainInitController.php
I am trying to access taxrate which should be indexAction() but I am doing something wrong since I am getting a Zend_Controller_Action_Exception. This is what I have tried all this URL combination so far:
http://localhos/admin/tax-rate/index
http://localhos/admin/tax-rate
http://localhos/admin/taxrate
http://localhos/admin/taxrate/index
And all of them generates the same error:
[message:protected] => Action "taxRate" does not exist and was not
trapped in __call()
This is the content of the class(es):
class TaxRateController extends MainInitController
{
public function indexAction()
{
echo 'I am here'; die();
}
}
class MainInitController extends Zend_Controller_Action {
....
}
What I am missing here? How I should be calling the controller/action?
Update 1:
I have tried to move the directory outside controllers but the result is the same:
application/
│   └── admin/
│   └── TaxRateController.php
├── controllers/
│   └── MainInitController.php
I am calling as http://localhost/admin/taxrate in this scenario.
With basic structure it will take time and effort to do that but it can be done
application/
├── controllers
│ └── admin
│ └── TaxRateController.php
You need to create routes for every controller under sub directory in your bootstrap:
public function _initAdminRoute()
$router = Zend_Controller_Front::getInstance()->getRouter();
// structure
$router->addRoute(
'unique_route_name',
new Zend_Controller_Router_Route('/admin/controllerRoute/:action/*',
['controller' => 'subdirName_controllerRoute']
)
);
// Like this
$router->addRoute(
'admin_taxrate_route',
new Zend_Controller_Router_Route('/admin/tax-rate/:action/*', ['controller' => 'admin_tax-rate'])
);
}
After this you need to rename your controller classes with subdirectory name to let zend find them. But do not change controller file names.
class TaxRateController => class Admin_TaxRateController
Now you can use your controllers, but a little fix may be needed for your views cause right now zend can not find your view directory. You need to move all your admin views to admin subdirectory or it will throw an error something similar to this.
Fatal error: Uncaught exception 'Zend_View_Exception' with message 'script 'admin/tax-rate/action.phtml' not found in path (application/views/scripts/)' in
Hope this helps, but still i will recommend using module structure.
If 'admin' is a module you should use a directory structure like this:
application/
├── modules
│ └── admin
| └── controllers
│ └── TaxRateController.php
Also make sure your application/configs/application.ini is according with it:
resources.frontController.moduleDirectory = APPLICATION_PATH "/modules/"
Edit
Be sure your file public/index.php ends like this:
$application = new Zend_Application(
APPLICATION_ENV,
APPLICATION_PATH . '/configs/application.ini'
);
$application->bootstrap()
->run();

PHPUnit test suite naming conventions

The PHPUnit manual highlights some conventions:
The tests for a class MyClass go into a class MyClassTest
The class MyClassTest live in file MyClassTest.php
MyClassTest inherits from PHPUnit_Framework_TestCase
Tests are public methods that are named test*
This will result in something like this folder structure:
├── src/
│ ├── classes/
│ │ ├── MyClass.php # Different
│ └── ...
├── tests/
│ ├── testcases/
│ │ ├── MyClassTest.php # Different
│ ├── bootstrap.php
│ └── ...
└── ...
... and this test case:
MyClassTest extends PHPUnit_Framework_TestCase {
testMyMethod() {
// Code here.
}
}
My question
I'm wondering if there is any reason why the naming used inside the test suite can't mirror the project's source code? For example, I'm thinking file names could match:
├── src/
│ ├── classes/
│ │ ├── MyClass.php # Same
│ └── ...
├── tests/
│ ├── testcases/
│ │ ├── MyClass.php # Same
│ ├── bootstrap.php
│ └── ...
└── ...
And if using PHP > 5.3, namespaces can be used to allow class names to match:
namespace MyProject\MyTests;
MyClass extends PHPUnit_Framework_TestCase { # The class name MyClass matches the class name used in my project's source.
/**
* #test
*/
MyMethod() { # The method name MyMethod matches the method name used in my project's source.
// Code here.
}
}
Note the #tests annotation is used so method names can match.
And if using PHP > 5.3, namespaces can be used to allow class names to match:
There are reasons not to do this:
It makes sense to have test and class under test in the same namespace
Otherwise you need to import the class under test with a class alias to distinguish it from the test case:
use MyProject\MyClass as MyActualClass;
The method name MyMethod matches the method name used in my project's source.
This might sound appealing if you think of testMyMethod as the alternative, but this is not the convention. Instead you should use more descriptive test method names like testThatMyMethodReturnsTrueIfFooIsBar.

Mustache_Engine not loading, conflict due to setting namespace?

The code below is me attempting to load Mustache into a Composer library (meaning the library itself is also being loaded by composer by the full project) I'm making for a project.
<?php
namespace TradeDefender\SiteEngine;
require '../../vendor/autoload.php';
class MessageEngine{
function test(){
$m = new Mustache_Engine;
return "hello";
}
}
?>
The directory structure for the library itself looks like this:
.
├── lib
│   └── TradeDefender
│   ├── Api
│   ├── Conn
│   └── SiteEngine
└── vendor
├── composer
└── mustache
I'm suspecting that it's due to me setting a namespace in the class, but I'm not sure how to fix it. The error itself is that it's not able to find the Class Mustache_Engine in the SiteEngine folder. The autoloader itself is being loaded just fine.
Any ideas? Thanks.
The problem was that I was loading the Mustache_Engine from the locally defined namespace rather than the global namespace. To load from the global namespace I had to put a \ infront of Mustache_Engine, like so:
$m = new \Mustache_Engine;

The `include` statement in a registered autoloader function runs in the context of which script?

Directory structure
learner#debian:~$ tree ~/bin
/home/learner/bin
└── php
├── Body
│   ├── Brain.php
│   └── Cell
│   └── Neuron.php
└── main.php
3 directories, 3 files
First code example
~/bin/php/main.php:
<?php
spl_autoload_register(function ($class) {
$path = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
echo "----- Autoload $class from $path -----\n";
include $path;
});
use Body\Brain;
$brain = new Brain();
?>
~/bin/php/Body/Brain.php:
<?php
namespace Body;
use Body\Cell\Neuron;
class Brain
{
public function __construct()
{
$this->brain = new Neuron();
$this->brain->talk();
}
}
?>
~/bin/php/Body/Cell/Neuron.php:
<?php
namespace Body\Cell;
class Neuron
{
public function talk()
{
echo "I am Neuron!\n";
}
}
?>
I am able to execute main.php from my home directory in the following
manner and it works fine:
learner#debian:~$ php ~/bin/php/main.php
----- Autoload Body\Brain from Body/Brain.php -----
----- Autoload Body\Cell\Neuron from Body/Cell/Neuron.php -----
I am Neuron!
I am surprised that this works. I expect it to fail when
$this->brain = new Neuron(); is executed in
~/bin/php/Body/Brain.php. When this line is encountered, the
autoloader tries to execute include 'Body/Cell/Neuron.php' but there
is no such subdirectory called Body inside ~/bin/php/Body.
Second code example
Let me show you why I expect the first code example to fail by showing another code example that is using include statements instead of autoloader.
~/bin/php/main.php modified to:
<?php
include 'Body/Brain.php';
use Body\Brain;
$brain = new Brain();
?>
See that the autoloader is missing now from the above code, and the same
include statement is being used now which was being executed by the
autoloader earlier.
~/bin/php/Body/Brain.php modified to:
<?php
namespace Body;
include 'Body/Cell/Neuron.php';
use Body\Cell\Neuron;
class Brain
{
public function __construct()
{
$this->brain = new Neuron();
$this->brain->talk();
}
}
?>
Note that the same include statement has been added to this code that
was being executed by the autoloader earlier to load Body\Cell\Neuron.
Trying to execute this code results in failure.
learner#debian:~$ php ~/bin/php/main.php
PHP Warning: include(Body/Cell/Neuron.php): failed to open stream: No such file or directory in /home/learner/bin/php/Body/Brain.php on line 4
PHP Warning: include(): Failed opening 'Body/Cell/Neuron.php' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /home/learner/bin/php/Body/Brain.php on line 4
PHP Fatal error: Class 'Body\Cell\Neuron' not found in /home/learner/bin/php/Body/Brain.php on line 12
This failure is expected because while executing
include 'Body/Cell/Neuron.php'; in ~/bin/php/Body/Brain.php, it
doesn't find a subdirectory called Body in ~/bin/php/Body.
Question
I know that I can easily fix the second code example by editing
~/bin/php/Body/Brain.php to use
include 'Cell/Neuron.php'; instead of
include 'Body/Cell/Neuron.php';. However, my question is not about
why the second code example doesn't work, but about why the first code
example works.
When the PHP interpreter is unable to include 'Body/Cell/Neuron.php'
from ~/bin/php/Body/Brain.php in the second code example, how
does the autoloader succeed in doing the same include from the same
PHP file in the first code example?
Or am I mistaken? Could it be that in the first code example the
autoloader is always executing the include statements from
~/bin/php/main.php regardless of where the classes are first
being used, so include 'Body/Cell/Neuron.php' is done at
~/bin/php/main.php and it succeeds because there indeed is a
subdirectory called Body in ~/bin/php? If this is the case,
where can I read about it in the official documentation?
To summarize our understanding, could you please tell me where the
registered autoloader function is executed from, in general? From the script
where the function is defined? From the script where the function
is registered? Or from the script where a new class is encountered?
Okay it's so:
In your first code example you are defining the autoloader. It will include every needed class with include. Imagine it just copy and pastes the code in currently running php script.
Now in the Brain.php file (which has been included, let me say: actually it's running in main.php because it has been "copy and pasted" into main.php) the class Neuron is needed. In main.php the autoloader is denfined -> the autoloader is called and loads the Neuron class. I think you understand.
Your second example doesn't work because when you are going to include Brain.php the PHP parser will before including Brain.php into main.php, try to include Body/Cell/Neuron.php into Brain.php. And you know that this can't work.
The autloader is excuted from/in the script you placed it. In your example in main.php
(I hope I hasn't confused you more)
I am answering my own question here. With some experiments it appears to me that the registered autoloader function executes in the context of the script where it is defined, and not where it is registered as an autoloader, and not where new classes are instantiated or used.
However, since this is only an experiment that tries to prove my theory, if anyone can answer my question by quoting official documentation, I'll mark that as the correct answer.
Experiment setup
Directory structure:
learner#debian:~$ tree ~/bin
/home/learner/bin
└── php
├── AutoLoader
│   ├── AutoLoader.php
│   └── ClassLoader
│   └── ClassLoader.php
├── Body
│   ├── Brain.php
│   └── Cell
│   └── Neuron.php
└── main.php
5 directories, 5 files
~/bin/php/AutoLoader/ClassLoader/ClassLoader.php contains the autoload function called ClassLoader::loadClass.
~/bin/php/AutoLoader/AutoLoader.php registers the autoload function by calling spl_autoload_register('Autoloader\\ClassLoader\\ClassLoader::loadClass');.
Source code
~/bin/php/AutoLoader/AutoLoader.php:
<?php
namespace AutoLoader;
include 'ClassLoader/ClassLoader.php';
class AutoLoader
{
static public function registerAutoloader()
{
spl_autoload_register(
'Autoloader\\ClassLoader\\ClassLoader::loadClass');
}
}
?>
~/bin/php/AutoLoader/ClassLoader/ClassLoader.php:
~/bin/php/Body/Brain.php:
<?php
namespace Body;
use Body\Cell\Neuron;
class Brain
{
public function __construct()
{
$this->brain = new Neuron();
$this->brain->talk();
}
}
?>
~/bin/php/Body/Cell/Neuron.php:
<?php
namespace Body\Cell;
class Neuron
{
public function talk()
{
echo "I am Neuron!\n";
}
}
?>
~/bin/php/main.php:
<?php
include 'AutoLoader/AutoLoader.php';
use AutoLoader\AutoLoader;
use Body\Brain;
AutoLoader::registerAutoloader();
$brain = new Brain();
?>
Experiment 1
Executing the above code results in error:
learner#debian:~$ php ~/bin/php/main.php
----- Autoload Body\Brain from Body/Brain.php -----
PHP Warning: include(Body/Brain.php): failed to open stream: No such file or directory in /home/learner/bin/php/AutoLoader/ClassLoader/ClassLoader.php on line 9
PHP Warning: include(): Failed opening 'Body/Brain.php' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /home/learner/bin/php/AutoLoader/ClassLoader/ClassLoader.php on line 9
PHP Fatal error: Class 'Body\Brain' not found in /home/learner/bin/php/main.php on line 8
Observation: The autoloader could not include Body/Brain.php from ~/bin/php/Body/Brain.php.
Experiment 2
Moving Body directory to ~/bin/php/AutoLoader and executing the code still results in error:
learner#debian:~$ mv ~/bin/php/Body ~/bin/php/AutoLoader
learner#debian:~$ tree ~/bin
/home/learner/bin
└── php
├── AutoLoader
│   ├── AutoLoader.php
│   ├── Body
│   │   ├── Brain.php
│   │   └── Cell
│   │   └── Neuron.php
│   └── ClassLoader
│   └── ClassLoader.php
└── main.php
5 directories, 5 files
learner#debian:~$ php ~/bin/php/main.php
----- Autoload Body\Brain from Body/Brain.php -----
PHP Warning: include(Body/Brain.php): failed to open stream: No such file or directory in /home/learner/bin/php/AutoLoader/ClassLoader/ClassLoader.php on line 9
PHP Warning: include(): Failed opening 'Body/Brain.php' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /home/learner/bin/php/AutoLoader/ClassLoader/ClassLoader.php on line 9
PHP Fatal error: Class 'Body\Brain' not found in /home/learner/bin/php/main.php on line 8
Observation: The autoloader could not include Body/Brain.php from ~/bin/php/AutoLoader/Body/Brain.php.
Experiment 3
Moving Body directory to ~/bin/php/AutoLoader/ClassLoader and executing the code results in success.
learner#debian:~$ mv ~/bin/php/AutoLoader/Body ~/bin/php/AutoLoader/ClassLoader/Body
learner#debian:~$ tree ~/bin
/home/learner/bin
└── php
├── AutoLoader
│   ├── AutoLoader.php
│   └── ClassLoader
│   ├── Body
│   │   ├── Brain.php
│   │   └── Cell
│   │   └── Neuron.php
│   └── ClassLoader.php
└── main.php
5 directories, 5 files
learner#debian:~$ php ~/bin/php/main.php
----- Autoload Body\Brain from Body/Brain.php -----
----- Autoload Body\Cell\Neuron from Body/Cell/Neuron.php -----
I am Neuron!
Observation: The autoloader could include Body/Brain.php from ~/bin/php/AutoLoader/ClassLoader/Body/Brain.php.
Conclusion
The conclusions of the three experiments seem to indicate that the autoloader is including files with respect to ~/bin/php/AutoLoader/ClassLoader/ directory, i.e. the directory that contains the script that defines the autoloader function.

Categories