-
Notifications
You must be signed in to change notification settings - Fork 7
Autoloading
Autoloading is an integral part of Composer and the "new PHP". It can solve some of the problems that exist when trying to execute a package's code in both production and development environments when the directory names and structure may be different.
For autoloading to work, you need to do a few things:
- Organize your project so your classnames and PHP namespaces correspond to your directory structure. The standard Repoman Folder-Structure will be compatible with autoloading.
- Define a valid "autoload" scheme in your composer.json file.
- Run
composer dump-autoload
to ensure that the autoload files are regenerated. (Alternatively, you can go through thecomposer install
or re-runcomposer update
to update things in addition to the autoload files). - Include your project's
vendor/autoload.php
file in any code where you want classes to be autoloaded.
Let's go through this to-do list, starting with the last item because it is the simplest.
Remember the trick to including any file when using Repoman is to leverage getOption so it returns your local directory when developing and the production directory when the code is installed via package. Here you can see an example of including your vendor/autoload.php
file:
$core_path = $modx->getOption('myvendor/mypkg.core_path','', MODX_CORE_PATH.'components/mypkg/');
require_once $core_path.'vendor/autoload.php';
In a perfect world, this is the only file you would need to include for PHP to find all of your classes. Review the page on Coding Conventions for more detail here.
Let's look at a couple common scenarios.
You may not encounter this until you start working with custom database tables and their counterpart xPDO classes. The MODX standard is to put all of xPDO's Object-Relational-Mapping (ORM) classes into your packages model/
directory. You can generate your xPDO class files in the standard locations and then use the autoload "classmap" attribute in your composer.json
:
"autoload": {
"classmap":["model/mypkg/"]
}
Once you run composer dump-autoload
and include the vendor/autoload.php
, any classes you have inside your model/mypkg/
directory can simply be instantiated with no need to include them first.
For example, your file might be model/mypkg/something.class.php
, and it contains <?php class Something { ... } ?>
, inside your Plugins, Snippets or other code, you can use that class simply by referencing the class name:
<?php
// as long as you've included vendor/autoload.php somewhere, instantiation will automatically trigger
// the file to be included:
$Something = new Something();
?>
If you have more than one subfolder for your model classes, simply add them to the autoload classmap array and re-run composer update
:
"autoload": {
"classmap":[
"model/mypkg/",
"model/subpkg/",
"model/somethingelse/"
]
}
The most powerful use of autoloading (in my opinion) comes when you use PHP namespaces. Remember: these are not MODX namespaces. PHP namespaces help avoid naming collisions that would otherwise occur if you tried to load two classes with the same name.
To use PHP namespaces, declare the namespace at the beginning of your class file:
<?php
namespace MyPkg;
class Something {
// ... etc...
?>
To instantiate this class, you would need to include the file, and then instantiate it like this:
<?php
$Obj = new MyPkg\Something();
?>
It takes some time to get used to the backslash. In order to have these classes discovered by autoloading, the recommended usage is to rely on the PSR-4 naming specification. I'll spare you the headaches and cut right to an example.
Imagine this "MyPkg\Something" class is saved inside model/Something.php
. You need to tell composer's autoloading to look there by defining the following autoload:
"autoload": {
"psr-4": {"MyPkg\\": "model/"}
}
If your project uses sub-folders, they need to also use sub-namespaces. For example, a file saved as model/sub/Thing.php
would need to declare its namespace and class as:
<?php
namespace MyPkg\Sub;
class Thing {
in order to be located by PSR-4 autoloading. Instantiating a class like that would need to include the sub-namespace:
$Obj = new MyPkg\Sub\Thing();
Pay close attention to a couple things: you need to use double back-slashes when referencing the classname in your composer.json, i.e. "MyPkg\" not "MyPkg" -- this is because a backslash escapes the character that follows it, so you end up with invalid JSON if don't use double backslashes.
Do not using ".class.php" as an extension when using PSR-4 autoloading. That particular MODX convention that is not compatible with PSR-4 autoloading, so if you decide to use PHP namespaces and autoload your classes this way, then you are forgoing usage of MODX's simple getService loading mechanism.
It's perfectly acceptable to mix both "classmap" and "psr-4" autoloading in your project. Consider the following example:
"autoload": {
"classmap":[
"model/orm/moxycart/",
"model/orm/foxycart/"
],
"psr-4": {"Moxycart\\": "model/"}
}
In this case, I have moved xPDO's ORM class files from the standard locations inside of model/
into a dedicated orm/
directory. This is because they are not traditional model classes which you might be used to if you have worked with an MVC framework such as CodeIgniter or Laravel. xPDO's ORM files define objects and attributes at a very granular level and those classes are therefore not ideal for housing functions that operate on your collections as a whole.
WARNING: Remember to run composer dump-autoload
after editing your composer.json autoload configuration.
You need to keep a couple caveats in mind regarding the "use" keyword and MODx, particularly when it comes to Plugins and Snippets. The thing to remember is that all of your Plugin and Snippet code is cached as a function before it is executed, even if you are using a static element.
That means that a Snippet that tries to use the "use" keyword:
<?php
/**
* @name MySnippet
*/
use \Michelf\Markdown; // <-- Illegal!
return Markdown::defaultTransform($content);
Actually is run from a cached file like this from inside core/cache/includes/elements/modsnippet
:
<?php
function elements_modsnippet_5($scriptProperties= array()) {
global $modx;
if (is_array($scriptProperties)) {
extract($scriptProperties, EXTR_SKIP);
}
/**
* @name MySnippet
*/
use \Michelf\Markdown; // <-- Illegal!
return Markdown::defaultTransform($content);
And that generates an error because:
The use keyword must be declared in the outermost scope of a file (the global scope) or inside namespace declarations.
(From the PHP Manual)
You cannot use the "use" keyword directly inside your Plugins or Snippets. You must either include another file that makes use of this keyword, or you must provide the fully qualified classname.
So instead you can use the fully qualified classname, e.g.
<?php
/**
* @name MySnippet
*/
// Instead, use the fully qualified classname
return \Michelf\Markdown::defaultTransform($content);
Or, you would need to include a separate file from your Snippet, e.g.
<?php
/**
* @name MySnippet
*/
$core_path = $modx->getOption('myvendor/mypackage.core_path','', MODX_CORE_PATH.'components/mypackage/');
require_once $core_path.'vendor/autoload.php';
// loaded by the autoloader if you have specified the proper autoloader classmap in your composer.json
$MyModel = new MyModel();
return $MyModel->do_something();
The same general issue affects plugins. See Issue 11129
All of this begs the question of where can you put your one include statement to rule them all? I recommend including your vendor/autoload.php
from inside a MODX Plugin.
-
MODX 2.2.x : the OnInitCulture event is the event that fires the earliest, but this may not be early enough to handle every case (e.g. Dashboard widgets might execute prior to this event), and any command-line usage (e.g. for unit tests) will need to explicitly include the
autoload.php
file. -
MODX 2.3 : This version of MODX will have an "init" event. Stay tuned.
Another possible solution is to include the file from inside a valid metadata.mysql.php
file. These files get included when you run the addPackage or addExtensionPackage functions, so this location is viable so long as your package is included in MODX's list of extension_packages
(see the system setting). Just remember that this file can get overwritten if you change your database schema, but it is a file that gets loaded very early on and it is under your control. Although this works, it's hardly transparent: virtually nobody will think to look inside that file to see how and where you are autoloading your classes.
You may also consider adding the include 'vendor/autoload.php';
statements to any code that needs to include other files. It's not nearly as clean, but it is more visible.
- Do not modify your MODX config.inc.php file. This does get loaded during every request, but it's a poor solution for several reasons: unlike other systems, MODX will regenerate its config file during updates (everything in it except the contents of the
$config_options
and$driver_options
arrays). This means your modifications get overwritten. Besides, you can't write a package that would update the config file. - Do not modify your MODX index.php -- it's hacky. You cannot reliably deploy a package that requires custom modifications there.
© 2014 and beyond by Craftsman Coding