How would you connect slimphp with modx to build a REST API

How would you connect slimphp with modx to build a REST API nowadays.
I found this

But this is 6 years old.

Or is this
https://docs.modx.com/current/en/extending-modx/developing-restful-api
still the recommended way to build REST APIs connected to MODX?

MODSlim was intended to run MODX requests through Slim (and PSR7) in order to be able to make use of an app server that keeps code in memory and simply handles PSR7 requests as they come in. However, loading MODX in any Slim request is fairly simple and I personally would much rather build a REST API in Slim rather than in a CMS. I’ve built a number of REST APIs in Slim 3.x, but never one that connected to MODX, so I don’t have any specific examples ready for you, but I’d be happy to craft some if you need some assistance getting MODX working in your DI container. I’ve also built some stuff in Slim 4.x recently and getting going in 4.x would be similar effort if you’ve never created something with either.

2 Likes

If you’d like to run MODX behind Slim, such middleware or a notFound handler in Slim is pretty simple to accomplish. The rest of your endpoints can then be plain Slim (with access to the MODX/xPDO if needed through the container, like Jason says).

If you don’t want MODX behind Slim, you could also simply setup a separate folder for the API and route requests to that folder to Slim instead. Still with MODX/xPDO in the container to access models and settings if needed, but no need to route one through the other in that case.

Slim/custom API routes inside MODX… now there’s a dream to figure out some day :smiley: The modRestServer stuff is cool for a quick CRUD API for xPDO models, but it’s also a bit restrictive in not supporting subresources and such.

1 Like

No doubt being able to use MODX as a manager for Slim routing would be awesome. This is exactly why the original MAB proposal to reconstruct MODX on top of Slim was conceived.

2 Likes

I guess, what I want, is replacing the modRestServer stuff, which I’m using right now in my Extra, with a Slim based approach.
I want to manage the routing with MIGX - configurations, where I’ve stored classnames, joins, permissions, hooks and what not.

This is my first attempt with Slim 4, which basically seems to work.

\assets\components\slimtest\index.php

<?php

require_once dirname(__FILE__,4) . '/config.core.php';
require_once MODX_CORE_PATH . '/components/slimtest/public/index.php';

\core\components\slimtest\public\index.php

<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

$working_context = 'web';

require_once MODX_CORE_PATH . 'model/modx/modx.class.php';

$modx = new modX();
$modx->initialize($working_context);

$base_path = MODX_ASSETS_URL . 'components/slimtest';
//$_SERVER['REQUEST_URI'] = str_replace('/assets/components/slimtest/','',$_SERVER['REQUEST_URI']);

// Should be set to 0 in production
error_reporting(E_ALL);
// Should be set to '0' in production
ini_set('display_errors', '1');

$app = AppFactory::create();
$app->setBasePath($base_path);

$app->get('/resources/{id}', function (Request $request, Response $response, $args) {
    global $modx;
    $result = [];
    $status = 200; 
    if ($resource = $modx->getObject('modResource',$args['id'])) {
        $result = ['result' => $resource->toArray()];
    } else {
        $result = ['error' => ['message' => 'Resource not found']];
        $status = 404;
    }
    
    $response->getBody()->write(json_encode($result,JSON_PRETTY_PRINT));
    return $response
        ->withHeader('Content-Type', 'application/json')
        ->withStatus($status);
});

$app->run();

however, global $modx doesn’t feel right.
I guess, my question right now is, what is the right way, to bring MODX to the party in that case.

You could insert $modx into a container (I don’t remember off-hand if $app has one by default there), or use use to pass it into the context:

$app->get('/resources/{id}', function (Request $request, Response $response, $args) use ($modx) {

Thanks @markh use did work, if I work with a Closure, but when I try to use a Action - Class, I need to pass it into a container.
My experimental scenario looks like that now, which seems to work so far:

\core\components\slimtest\public\index.php

<?php
use DI\Container;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
use Slim\Psr7\Response;

require __DIR__ . '/../vendor/autoload.php';

// Create Container using PHP-DI
$container = new Container();

// Set container to create App with on AppFactory
AppFactory::setContainer($container);

$working_context = 'web';

require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
$modx = new modX();
$modx->initialize($working_context);

$container->set('modx', function () use ($modx) {
    return $modx;
});

$base_path = MODX_ASSETS_URL . 'components/slimtest';

// Should be set to 0 in production
error_reporting(E_ALL);
// Should be set to '0' in production
ini_set('display_errors', '1');

$app = AppFactory::create();
$app->setBasePath($base_path);

$app->get('[/{route}/{key}]',\App\Action\DefaultActions\ReadAction::class);

$app->run();

\core\components\slimtest\src\Action\DefaultActions\ReadAction.php

<?php

namespace App\Action\DefaultActions;

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class ReadAction {

    private $container;
    private $modx;
    private $response;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
        $this->modx = $container->get('modx');
    }    

    public function __invoke(ServerRequestInterface $request,ResponseInterface $response, array $args): ResponseInterface {
        $this->response = & $response;
        $this->query_params = $request->getQueryParams();  

        $modx = & $this->modx;
        $result = []; 
        $result['route'] = $args['route'];
        $result['key'] = $args['key'];
        $result['query_params'] = $this->query_params;
        
        $params = explode('/',$args['route']);
        $result['params'] = $params;
    
        $classname = $params[0];
       
        $status = 200; 
        if (isset($args['key']) && $object = $modx->getObject($classname,$args['key'])) {
            $result['result'] = $object->toArray();
        } else {
            $result['error'] = ['message' => 'Resource not found'];
            $status = 404;
        }
        return $this->prepareResponse($result,$status);

    } 

    private function prepareResponse($result,$status) {
        $this->response->getBody()->write(json_encode($result,JSON_PRETTY_PRINT));
        return $this->response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus($status);        
    }
    
}

not sure, if thats the right direction.

I’d put the bit inside of the $container->set() callback rather than the use syntax for a global $modx and probably suggest using the class name instead of a named string:

$container->set(modX::class, function () {
    $working_context = 'web';

    require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
    $modx = new modX();
    $modx->initialize($working_context);
    return $modx;
});

… but beyond that I think that’s pretty much how I’ve done it on projects as well.

At some point a project and container grows that you start initialising it differently to keep it more strucctured than a single big index.php, e.g. like this with a custom App class

Or you might choose to start making use of PHP-DIs autowiring so instead of pulling dependencies out of the container in the constructor, your constructor just looks like this instead:

class ReadAction {
    public function __construct(modX $modx)
    {
        $this->modx = $modx;
    }

    // ...
}

That should just work with the current way you’re defining the route, as long as you adjust the $container->set() call to use the class name.

1 Like