Custom file extensions (e.g. .env): routing and rendering serialized responses

in CakePHP


It is fairly common to add json handler to a CakePHP route and have it automatically respond with formatted JSON. Consider this example.

  • In your route:

    $routes->prefix('Api', function (RouteBuilder $routes) {
        $routes->setExtensions(['json']);
        $routes->fallbacks(DashedRoute::class);
    });
  • In your controller:

    public function initialize(): void
    {
        parent::initialize();
        $this->addViewClasses([\Cake\View\JsonView::class]);
        $this->viewBuilder()->setOption('serialize', true);
    }

So now when you request actions in that controller as /controller/action.json, they will respond with their view variables serialized into JSON.

But what if you want to have formatted responses to a custom extension?

For example, /controller/action.env responding with KEY=VALUE?

1. Tell CakePHP router about your new extension

In your config/routes.php:

$routes->prefix('Api', function (RouteBuilder $routes) {
    $routes->setExtensions(['json', 'env']);
    $routes->fallbacks(DashedRoute::class);
});

2. Tell CakePHP controller about view class for the extension

In your controller:

public function initialize(): void
{
    parent::initialize();
    $this->addViewClasses([\Cake\View\JsonView::class, \App\View\EnvView::class]);
    $this->viewBuilder()->setOption('serialize', true);
}

Important:

  • The EnvView does not exist yet - we will create it in the next step.
  • The above only makes CakePHP aware of your custom view class, but you may also have to explicitly link it to the file extention you want it to handle. See #4 below.

3. Create the actual view class

In src/View/EnvView.php, define class EnvView extending \Cake\View\SerializedView. The class must have these two methods:

  • contentType()
  • _serialize()
<?php

namespace App\View;

class EnvView extends \Cake\View\SerializedView
{

    /**
     * Mime-type this view class renders as.
     *
     * @return string The plain text content type.
     */
    public static function contentType(): string
    {
        return 'text/plain';
    }

    /**
     * @inheritDoc
     */
    protected function _serialize(array|string $serialize): string
    {
        if (is_string($serialize)) {
            $serialize = [$serialize];
        }

        // Serialize only the keys requested explicitly
        $data = array_intersect_key($this->viewVars, array_flip($serialize));

        $lines = [];
        foreach ($data as $key => $value) {
            $lines[] = sprintf('%s=%s', $key, \escapeshellarg($value));
        }

        return (string)implode(PHP_EOL, $lines);
    }

}

4. (Optional) Link your extension to a mime type

CakePHP controller matches requested file extension to a view class based on $mimeTypes array in src/Http/MimeType.php. The built-in list is pretty exhaustive, but it does not contain env from our example. So a necessary step is to explicitly map env to the mime type we return in EnvView::contentType(), which happens to be text/plain.

Update the controller initialize() to:

public function initialize(): void
{
    \Cake\Http\MimeType::addMimeTypes('env', \App\View\EnvView::contentType()); // ADD THIS
    parent::initialize();
    $this->addViewClasses([\Cake\View\JsonView::class, \App\View\EnvView::class]);
    $this->viewBuilder()->setOption('serialize', true);
}

Further reading:

#cakephp #cakephp-routing