Managing and Deploying custom PHP services in Dreamfactory

If you are using custom scripted services or event scripts in Dreamfactory you might have noticed its not ideal how to install and maintain them. Uploading individual files via browser, the system/ API or directly editing them in Dreamfactory is error-prone. Its also not fitting very well into an automated environment.

Read on after the jump how to manage and deploy your custom scripted PHP services in a sane manner. The same way Dreamfactory installs its own modules, utilizing PHP composer

First of all I’d like to point out that its really a good idea to use PHP for custom scripted services. Even if PHP isnt the best choice in general and using Node.js or Python is tempting. Key benefits with PHP would be that Dreamfactory itself is written in PHP Laravel Framework and you can directly leverage it then. See my other post for an exmple: Adding custom session data to Dreamfactory JWT token

One undocumented limitation with DF scripts in general is their length which is limited to 65k characters. We hit that limit at ~1500 lines of code when suddenly the entire service was broken. Unfortuantely you cannot simply include/require other PHP files as the scripts are eval()-ed.
Whats working however, is splitting the services into classes and making your own package thats going to be installed via PHP composer.

Status Quo

So what you likely will have as of now is sth like this

<?php
$resource = $event['resource'];

// get query params from request
$params = $event['request']['parameters'];

$required = ['n1', 'n2'];

if(!empty($resource)){
    foreach($required as $element){
        if(!isset($params[$element])){
            throw new \DreamFactory\Core\Exceptions\BadRequestException('Missing '.$element.' in params.');
        }
    }
    $n1 = $params['n1'];
    $n2 = $params['n2'];
}

switch ($resource) {
    case "add":
        $result = ['result' => ($n1 + $n2)];
        break;
    case "subtract":
        $result = ['result' => ($n1 - $n2)];
        break;
    default:
        throw new \DreamFactory\Core\Exceptions\BadRequestException('Invalid or missing resource name.');
        break;
}

return $result;

Of course your endpoints (e.g. add, substract..) have much more code.

Refactoring

Now, refactoring this into an installable package isnt to hard. First create a folder structure like this

src/
├── FancyProject/
|   ├── Math/
|   |   └── Math.php
|   └── Consumer/
|       ├── Newsletter.php
|       └── Product.php
└── composer.json

You can leave out the “Consumers” folder. I only put it there to show you that you can further split down and logically group your service endpoints if you like. Which makes sense if you have alot.

Assume a custom scripted service called “consumers” and the service endpoints would be “newsletter/subscribe” , “newsletter/unsubscribe”, “products/list”, “products/info” et.al. Then you would have two classes and files “Newsletter” and “Product”.

Now fill composer.json with the following content

{
    "name": "fancyproject/services",
    "autoload": {
        "psr-4" : {
            "FancyProject\\Math\\" : "src/",
            "FancyProject\\Consumer\\" : "src/"
        }
    }
}

All this does is telling the PHP Autoloader where to find your classes so the refactored code can work the way we reference (=include) our classes.

Here is the Math.php

<?php
namespace FancyProject\Math;

class Math
{
    protected $platform;
    protected $event;
    protected $payload;
    protected $parameters
    protected $errorCode;
    protected $errorMessage;
    protected $result;

    // Dreamfactorys objects "platform" and "event" are passed in via constructor
    public function __construct($platform, $event)
    {
        $this->platform     = $platform;
        $this->event        = $event;
        $this->payload      = $event['request']['payload'];    // for easy-access
        $this->parameters   = $event['request']['parameters'];
        $this->errorCode    = 0;
        $this->errorMessage = "";
        $this->result       = NULL;
      }

    public function getLastErrorCode() { return $this->errorCode; }
    public function getLastErrorMessage() { return $this->errorMessage; }
    public function getResult() { return $this->result; }

    // functions MUST return either true or false. nothing else
    public function add()
    {
        $result = ['result' => ($this->parameters['n1'] + $this->parameters['n2'])];

        $this->result = $result;

        return true;
    }

    // if you have variables only relevant for certain endpoints, pass them in as arguments
    public function someEndpoint()
    {
        $res = $this->platform['api']->get->__invoke("some-service");

        // if you need error handling, do it like this
        if(empty($res)) {
            $this->errorCode = 1052;
            $this->errorMessage = "Some error";
            return false;
        } else {
            $this->result = $res;
        }

        return true;
    }
}

Some explaining words

  • DF lookups like {some-lookup} are NOT working in the class. They have to be passed in as function arguments !
  • $result needs to be saved in $this->result and then return true;
  • error code/message need to be saved in $this->errorCode / $this->errorMessage and then return false;
  • refactoring your existing service endpoints is mostly copy&paste and then fixing $platform to $this->platform and $event to $this->event

Installing the package

Got to your Dreamfactory installation directory and run


# the path needs to point to the folder where you created the src/ folder in
composer config repositories.fancyproject path ./fancyproject
composer require fancyproject/services

# for updating, you will later run
composer update fancyproject/services

This will install/update your package into Dreamfactory.

If there is interest, I will tell you how to put the code into a (private) repository, version it and have composer install the version matching your version constraint. Perfect for automatic deployments !

Using the package

Wow, so much things done and its still not used as of now. Lets change that ! Remember the services code from above? Below you can see how the refactored “add” endpoint will look like



switch ($resource) {
    case "add":
        $math = new \FancyProject\Math\Math($platform, $event);

        if($math->add()) {
            $result = $math->getResult();
        } else {
            $event['response']['status_code'] = 400;
            $event['response']['content'] = ['error' => $math->getLastErrorMessage(), 'code' => $math->getLastErrorCode()];
            return;
        }
        break;

The class function “add” always returns true but the if/else shows you how you would do it for an endpoint with error handling like the “someEndpoint()”

Conclusion

When having all service endpoints refactored to classes, the custom scripted service wont change often and will act like a stub. Only the classes will change. This will also keep the script itself quite short and you wont hit the length limit which might happen in a moment where you really dont have the time for refactoring.

Another benefit is how this approach perfectly fits into an automated environment:
I put the package into a repository and tag commits with version numbers following Semantic Versioning . Furthermore I have modified the Dreamfactory Dockerfile to tell composer to install my custom package along all the DF packages. In addition I have a version constraint which ensures it will always install the latest minor version.

If you are curious about how to run Dreamfactory with Docker, have a look at Using Dreamfactory 2.x as REST API with Docker and Orchestrating Dreamfactory with docker-compose and a LoadBalancer

  1. No trackbacks yet.