Adding Custom Code Libraries in a Package

Improvements?

Let us know by posting here.

Packages will often need to include custom PHP code for some of their functionality. Concrete CMS's implementation of autoloading provides a few ways of keeping custom classes names attractive and easy to work with.

Structuring Your Package

Consider a scenario where your company, PortlandLabs, develops a product named Statistics. A logical namespace for your add-on's custom code would be PortlandlabsStatistics. Within this namespace, you might create a class PageListener to handle the collection of page view statistics.

Directory Structure:

packages/
└── portlandlabs_statistics/
    ├── controller.php
    └── src/
        └── Page/
            └── PageListener.php

All of the autoloaded classes will be located under the src/ directory. Note: everything under the src/ directory must match the class casing exactly for autoloading to work correctly.

Defining the Namespace and Class

In PageListener.php, define the namespace and class as follows:

<?php
namespace PortlandlabsStatistics\Page;

class PageListener {
    public function __construct() {
        // ...
    }
}

The class name here is succinct and easy to remember, but for Concrete to autoload it, we need to tell it where to find the class by adding it to an autoloader registry.

Autoloading Custom Classes

To enable Concrete to autoload your custom classes, specify the autoloader registry in your package's controller.php:

<?php
namespace Concrete\Package\PortlandlabsStatistics;

use Concrete\Core\Package\Package;

class Controller extends Package {
    protected $pkgHandle = 'portlandlabs_statistics';
    protected $appVersionRequired = '9.0';
    protected $pkgVersion = '1.0';

    protected $pkgAutoloaderRegistries = [
        'src' => 'PortlandlabsStatistics',
    ];

    public function getPackageName() {
        return t('PortlandLabs Statistics');
    }

    public function getPackageDescription() {
        return t('Manages widgets for FooCorp.');
    }

    public function on_start() {
        $listener = new \PortlandlabsStatistics\Page\PageListener();
        $listener->addListeners();
    }
}

Utilizing Custom Classes in Other Package Components

Suppose you have another class that reports the accumulated statistics for a page. When referencing your custom classes elsewhere in your package, ensure you include the appropriate use statement

<?php
use PortlandlabsStatistics\Page\PageStats;

$stats = new PageStats();
echo $stats->getStats($page);

Common Pitfalls and Solutions

  • Namespace and Directory Alignment: Ensure that the namespace declaration in your PHP files matches the directory structure and case. For instance, a class located at packages/portlandlabs_statistics/src/Page/PageListener.php and mapped to PortlandlabsStatistics as above should have the namespace of PortlandlabsStatistics\Page\PageListener.

  • Autoloader Configuration: Verify that the protected $pkgAutoloaderRegistries class property in controller.php correctly maps the directory to the namespace.

  • Class Naming Conventions: Adhere to PSR-4 standards, using CamelCase for both directory names and class names within the src/ directory.

Example: Implementing a Custom Helper Class

Suppose you want to create a helper class Summary within your package.

Directory Structure:

packages/
└── portlandlabs_statistics/
    ├── controller.php
    └── src/
        └── Summary.php

Summary.php:

<?php
namespace PortlandlabsStatistics;

class Summary {
    public static function getSummary($path) {
        // implement summary
    }
}

Using Summary in controller.php:

<?php
namespace Concrete\Package\PortlandlabsStatistics;

use Concrete\Core\Package\Package;
use PortlandlabsStatistics\Summary;

class Controller extends Package {
    // ... 

    public function report($page) {
        return Summary::getSummary($page);
    }
}

Avoiding Legacy Namespacing

Another complementary approach is to modify Concrete's traditional expectations about autoloading paths. Concrete historically automatically generates namespaces based on the src/ directory. For example, if you want to include a custom Captcha library in your package, you'll normally need to make sure a file is present at packages/your_package/src/Captcha/CustomCaptchaController.php and its class is automatically generated as Concrete\Package\YourPackage\Src\Captcha\CustomCaptchaController.

To prevent Concrete from expecting classes under the \Src namespace, set the protected $pkgAutoloaderMapCoreExtensions class property to true in your package controller and move the newly automapped src/ files from packages/your_package/src to packages/your_package/src/Concrete. This disables legacy namespacing and nicer namespaces.

For example, in your controller, you set $pkgAutoloaderMapCoreExtensions to true:

<?php
namespace Concrete\Package\YourPackage;

use Concrete\Core\Package\Package;

class Controller extends Package {
    // ... 
    protected $pkgAutoloaderMapCoreExtensions = true;
    // ...
}

From there on, all files found in the src/ directory in your package no longer need to have \Src in their namespace. Your custom captcha controller's class would be

namespace Concrete\Package\YourPackage\Captcha;
class CustomCaptchaController {
}

And the file system would look like: packages/your_package/src/Concrete/Captcha/CustomCaptchaController.php.