My previous post walked through configuring and enabling plugins in phpScheduleIt. That’s great… but what if we need something custom to our environment? The most popular support requests I get are for changing different workflows – especially around the reservation process. Today I’m going to dive into how to write plugins for the pre reservation events. If you know some PHP and a bit of object oriented design, this should be pretty straightforward.

Note: The version of phpScheduleIt at the time of writing is 2.2. While the exact details explained in this post may not apply to future versions, the core concepts will.

Getting Started

phpScheduleIt’s plugin architecture is largely convention-based. This means that a few rules apply to all plugins.

  • The plugin package structure must contain the following:
    • A directory named after the plugin, containing all plugin code.
    • A file within that directory named after the plugin.
    • If there is a configuration file needed, it must be named after the plugin, with ending with the suffix .config.php.
  • The plugin class name must must match the file name (without the .php suffix).
  • Plugins follow the decorator pattern.
    • The plugin must extend or inherit the base class or interface. It must also accept it as the first argument to the constructor (we’ll go into the different classes and interfaces available for each plugin type later).

For example, if we’re writing a PreReservation plugin named PreReservationExample, we would create a directory named PreReservationExample. Within it, we create a file named PreReservationExample.php. This file would contain a single class named PreReservationExample, which would extend the base plugin class and accept an instance of that class as the first constructor argument.

OK, enough with the abstract examples. Let’s get into some code.

Pre-Reservation Classes and Interfaces

The pre-reservation plugin allows you to do all sorts of fun things like custom validations, reservation adjustments, notifications and so on. The base class for PreReservation plugins is a PreReservationFactory. Let’s take a look at the interface.

interface IPreReservationFactory
{
	/**
	 * @param UserSession $userSession
	 * @return IReservationValidationService
	 */
	public function CreatePreAddService(UserSession $userSession);

	/**
	 * @param UserSession $userSession
	 * @return IReservationValidationService
	 */
	public function CreatePreUpdateService(UserSession $userSession);

	/**
	 * @param UserSession $userSession
	 * @return IReservationValidationService
	 */
	public function CreatePreDeleteService(UserSession $userSession);
}

Simple enough, but this gives us the power to hook into events before adding, updating, or deleting a reservation. The only argument to each of these functions is a UserSession, which gives you some insight into who is making each of these calls. Each one of these functions returns an instance of an IReservationValidationService. Let’s look at that interface.

interface IReservationValidationService
{
	/**
	 * @param ReservationSeries|ExistingReservationSeries $series
	 * @return IReservationValidationResult
	 */
	public function Validate($series);
}

Even easier! This has one function: Validate(), which accepts either a ReservationSeries (during add) or an ExistingReservationSeries (during update and delete). This is executed during the add/update/delete event and returns an instance of an IReservationValidationResult. Continuing down the path, we see that this interface is also pretty simple.

interface IReservationValidationResult
{
	/**
	 * @return bool
	 */
	public function CanBeSaved();

	/**
	 * @return array[int]string
	 */
	public function GetErrors();

	/**
	 * @return array[int]string
	 */
	public function GetWarnings(); 
}

All of these classes and interfaces can be found in /lib/Application/Reservation/Validation

Putting It All Together

Let’s say that for add and update we want to enforce some rules based on the value of a custom attribute that we added. For delete, we’re happy with the default behavior. Since we already explained how to create the plugin structure for a plugin named PreReservationExample, let’s stick with that name. So the contents of /plugins/PreReservation/PreReservationExample/PreReservationExample.php would look something like this:

class PreReservationExample extends PreReservationFactory
{
    /**
     * @var PreReservationFactory
     */
    private $factoryToDecorate;

    public function __construct(PreReservationFactory $factoryToDecorate)
    {
        $this->factoryToDecorate = $factoryToDecorate;
    }

    public function CreatePreAddService(UserSession $userSession)
    {
        $base = $this->factoryToDecorate->CreatePreAddService($userSession);
        return new PreReservationExampleValidation($base);
    }

    public function CreatePreUpdateService(UserSession $userSession)
    {
        $base =  $this->factoryToDecorate->CreatePreUpdateService($userSession);
        return new PreReservationExampleValidation($base);
    }

    public function CreatePreDeleteService(UserSession $userSession)
    {
        return $this->factoryToDecorate->CreatePreDeleteService($userSession);
    }
}

Let’s step through this. Our class extends the PreReservationFactory. It also accepts an instance of a PreReservationFactory as the only argument to the constructor with a parameter named $factoryToDecorate. This will be a fully instantiated object, loaded up with all the rules needed during add/update/delete. Our constructor just holds onto that instance as a private field.

Each of the Create* functions are then responsible for returning a service to be used before their respective event. In our case, CreatePreAddService() and CreatePreUpdateService() get the base validation service, then add their validation service onto it. Since we don’t have any custom rules for delete, we simply return the base service.

This is an important point. To extend the base PreReservation behavior, we need to return the base service, or a decorated version of that service. If we want to completely replace the base behavior, simply return your own custom object.

Now comes the fun part, our implementation of CreatePreAddService() decorates the existing instance. We do this by getting the result of the base CreatePreAddService() function, then decorating it with our own validation object. We end up returning an instance of a PreReservationExampleValidation, giving it that base service to wrap. Now, we’ll need to create the PreReservationExampleValidation to handle our custom rule when adding and updating a reservation.

We start off by adding a new file to our plugin directory named PreReservationExampleValidation.php. This contains a single class which must implement IReservationValidationService

class PreReservationExampleValidation implements IReservationValidationService
{
	/**
	 * @var IReservationValidationService
	 */
	private $serviceToDecorate;

	public function __construct(IReservationValidationService $serviceToDecorate)
	{
		$this->serviceToDecorate = $serviceToDecorate;
	}

	/**
	 * @param ReservationSeries|ExistingReservationSeries $series
	 * @return IReservationValidationResult
	 */
	public function Validate($series)
	{
		$result = $this->serviceToDecorate->Validate($series);

		// don't bother validating this rule if others have failed
		if (!$result->CanBeSaved())
		{
			return $result;
		}

		return $this->EvaluateCustomRule($series);
	}

	/**
	 * @param ReservationSeries $series
	 * @return bool
	 */
	private function EvaluateCustomRule($series)
	{
		// make your custom checks here
		$configFile = Configuration::Instance()->File('PreReservationExample');
		$maxValue = $configFile->GetKey('custom.attribute.max.value');
		$customAttributeId = $configFile->GetKey('custom.attribute.id');

		$attributeValue = $series->GetAttributeValue($customAttributeId);

		$isValid = $attributeValue <= $maxValue;

		if ($isValid)
		{
			return new ReservationValidationResult();
		}

		return new ReservationValidationResult(false, "Value of custom attribute cannot be greater than $maxValue");
	}
}

This class is also a decorator, so it accepts a class to decorate as the first parameter to the constructor. Next, lets look at the Validate() method. The first thing we do is call down to our decorated Validate() method. This runs all of the existing PreReservation steps. Then, we check to see if we're in a valid state by checking the value of $result->CanBeSaved(). If everything is OK to this point, then we go ahead and run our custom validation.

Walking through the EvaluateCustomRule() function, we first get our plugin's configuration file, reading settings for the maximum value we allow and the id of the custom attribute we are interested in. Then, we get the current value of that attribute from the ReservationSeries. Finally, if the value is less than or equal to our configured maximum, we simply return a new ReservationValidationResult. This indicates that there are no issues. If the value provided exceeds our maximum, then we return a ReservationValidationResult, passing in false to indicate the failure and an error message to explain what exactly failed. The error message will be displayed to the user.

Configuring Your Plugin

If you wanted to use phpScheduleIt's provided configuration API, you would just need to create and register your config file. This would change the constructor of the PreReservationFactory to something like this:

public function __construct(PreReservationFactory $factoryToDecorate)
{
	$this->factoryToDecorate = $factoryToDecorate;

	require_once(dirname(__FILE__) . '/PreReservationExample.config.php');

	Configuration::Instance()->Register(
				dirname(__FILE__) . '/PreReservationExample.config.php',
				'PreReservationExample');
}

And here is our config file:

$conf['settings']['custom.attribute.max.value'] = '100';
$conf['settings']['custom.attribute.id'] = '3';

We won't go into the details of configuration here, but it's enough to say that configuration files are structured as arrays. All settings are part of the $conf['settings'] array.

Wrapping It Up

As with any plugin, you'll need to tell phpScheduleIt that you want to use it by setting the plugin value in /config/config.php. Our value would be: $conf['settings']['plugins']['PreReservation'] = 'PreReservationExample';.

Hopefully this gives you a taste of what's possible. This is a powerful customization point and there's almost no limit to what you can do here. Also remember that you have all of phpScheudleIt's internal API available to you. Explore it and use it.

You can find the source code for this example in /plugins/PreReservation/PreReservationExample

Happy Coding!