{"id":1788,"date":"2026-01-11T16:58:06","date_gmt":"2026-01-11T16:58:06","guid":{"rendered":"https:\/\/editjournal.redakt.eu\/faxmodem\/?p=1788"},"modified":"2026-01-11T17:09:43","modified_gmt":"2026-01-11T17:09:43","slug":"routing-rendering-serialized-custom-extensions-env","status":"publish","type":"post","link":"https:\/\/editjournal.redakt.eu\/faxmodem\/blog\/development\/cakephp\/routing-rendering-serialized-custom-extensions-env\/","title":{"rendered":"Custom file extensions (e.g. .env): routing and rendering serialized responses"},"content":{"rendered":"<p>It is fairly common to add <code>json<\/code> handler to a CakePHP route and have it automatically respond with formatted JSON. Consider this example.<\/p>\n<ul>\n<li>\n<p>In your route:<\/p>\n<pre><code>$routes->prefix('Api', function (RouteBuilder $routes) {\n    $routes->setExtensions(['json']);\n    $routes->fallbacks(DashedRoute::class);\n});<\/code><\/pre>\n<\/li>\n<li>\n<p>In your controller:<\/p>\n<pre><code>public function initialize(): void\n{\n    parent::initialize();\n    $this->addViewClasses([\\Cake\\View\\JsonView::class]);\n    $this->viewBuilder()->setOption('serialize', true);\n}<\/code><\/pre>\n<\/li>\n<\/ul>\n<p>So now when you request actions in that controller as <code>\/controller\/action.json<\/code>, they will respond with their view variables serialized into JSON.<\/p>\n<p>But what if you want to have formatted responses to a custom extension?<\/p>\n<p>For example, <code>\/controller\/action.env<\/code> responding with <code>KEY=VALUE<\/code>?<\/p>\n<h3>1. Tell CakePHP router about your new extension<\/h3>\n<p>In your <code>config\/routes.php<\/code>:<\/p>\n<pre><code>$routes->prefix('Api', function (RouteBuilder $routes) {\n    $routes->setExtensions(['json', 'env']);\n    $routes->fallbacks(DashedRoute::class);\n});<\/code><\/pre>\n<h3>2. Tell CakePHP controller about view class for the extension<\/h3>\n<p>In your controller:<\/p>\n<pre><code>public function initialize(): void\n{\n    parent::initialize();\n    $this->addViewClasses([\\Cake\\View\\JsonView::class, \\App\\View\\EnvView::class]);\n    $this->viewBuilder()->setOption('serialize', true);\n}<\/code><\/pre>\n<p><strong>Important<\/strong>:<\/p>\n<ul>\n<li>The <code>EnvView<\/code> does not exist yet - we will create it in the next step.<\/li>\n<li>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.<\/li>\n<\/ul>\n<h3>3. Create the actual view class<\/h3>\n<p>In <code>src\/View\/EnvView.php<\/code>, define class <code>EnvView<\/code> extending <code>\\Cake\\View\\SerializedView<\/code>. The class must have these two methods:<\/p>\n<ul>\n<li><code>contentType()<\/code><\/li>\n<li><code>_serialize()<\/code><\/li>\n<\/ul>\n<pre><code>&lt;?php\n\nnamespace App\\View;\n\nclass EnvView extends \\Cake\\View\\SerializedView\n{\n\n    \/**\n     * Mime-type this view class renders as.\n     *\n     * @return string The plain text content type.\n     *\/\n    public static function contentType(): string\n    {\n        return &#039;text\/plain&#039;;\n    }\n\n    \/**\n     * @inheritDoc\n     *\/\n    protected function _serialize(array|string $serialize): string\n    {\n        if (is_string($serialize)) {\n            $serialize = [$serialize];\n        }\n\n        \/\/ Serialize only the keys requested explicitly\n        $data = array_intersect_key($this-&gt;viewVars, array_flip($serialize));\n\n        $lines = [];\n        foreach ($data as $key =&gt; $value) {\n            $lines[] = sprintf(&#039;%s=%s&#039;, $key, \\escapeshellarg($value));\n        }\n\n        return (string)implode(PHP_EOL, $lines);\n    }\n\n}<\/code><\/pre>\n<h3>4. (Optional) Link your extension to a mime type<\/h3>\n<p>CakePHP controller <a href=\"https:\/\/github.com\/cakephp\/cakephp\/blob\/c0175d821a5e42b934ad2a11927c5c5d0e659a0d\/src\/Controller\/Controller.php#L775\">matches<\/a> requested file extension to a view class based on <a href=\"https:\/\/github.com\/cakephp\/cakephp\/blob\/c0175d821a5e42b934ad2a11927c5c5d0e659a0d\/src\/Http\/MimeType.php#L49\"><code>$mimeTypes<\/code> array in <code>src\/Http\/MimeType.php<\/code><\/a>. The built-in list is pretty exhaustive, but it does not contain <code>env<\/code> from our example. So a necessary step is to explicitly map <code>env<\/code> to the mime type we return in <code>EnvView::contentType()<\/code>, which happens to be <code>text\/plain<\/code>.<\/p>\n<p>Update the controller <code>initialize()<\/code> to:<\/p>\n<pre><code>public function initialize(): void\n{\n    \\Cake\\Http\\MimeType::addMimeTypes('env', \\App\\View\\EnvView::contentType()); \/\/ ADD THIS\n    parent::initialize();\n    $this->addViewClasses([\\Cake\\View\\JsonView::class, \\App\\View\\EnvView::class]);\n    $this->viewBuilder()->setOption('serialize', true);\n}<\/code><\/pre>\n<p>Further reading:<\/p>\n<ul>\n<li><a href=\"https:\/\/book.cakephp.org\/5\/en\/development\/routing.html#prefix-routing\">https:\/\/book.cakephp.org\/5\/en\/development\/routing.html#prefix-routing<\/a><\/li>\n<li><a href=\"https:\/\/book.cakephp.org\/5\/en\/development\/routing.html#routing-file-extensions\">https:\/\/book.cakephp.org\/5\/en\/development\/routing.html#routing-file-extensions<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>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(&#8216;Api&#8217;, function (RouteBuilder $routes) { $routes->setExtensions([&#8216;json&#8217;]); $routes->fallbacks(DashedRoute::class); }); In your controller: public function initialize(): void { parent::initialize(); $this->addViewClasses([\\Cake\\View\\JsonView::class]); $this->viewBuilder()->setOption(&#8216;serialize&#8217;, true); } So now when you request actions in that&hellip; <a class=\"more-link\" href=\"https:\/\/editjournal.redakt.eu\/faxmodem\/blog\/development\/cakephp\/routing-rendering-serialized-custom-extensions-env\/\">Continue reading <span class=\"screen-reader-text\">Custom file extensions (e.g. .env): routing and rendering serialized responses<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1047],"tags":[715,1270],"class_list":["post-1788","post","type-post","status-publish","format-standard","hentry","category-cakephp","tag-cakephp","tag-cakephp-routing","entry"],"_links":{"self":[{"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/posts\/1788","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/comments?post=1788"}],"version-history":[{"count":4,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/posts\/1788\/revisions"}],"predecessor-version":[{"id":1792,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/posts\/1788\/revisions\/1792"}],"wp:attachment":[{"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/media?parent=1788"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/categories?post=1788"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/tags?post=1788"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}