DerEuroMark View RSS

A blog about Frameworks (CakePHP), MVC, Snippets, Tips and more
Hide details



Djot PHP: A Modern Markup Parser 8 Dec 8:59 PM (5 days ago)

If you’ve ever wished Markdown was a bit more consistent and feature-rich, you’ll want to hear about Djot – and now there’s a complete PHP implementation available.

What is Djot?

Djot is a lightweight markup language by the author of Commonmark (Markdown) and Pandoc. It takes the best ideas from Markdown while addressing many of its ambiguities and limitations. The syntax is familiar yet more predictable, making it an excellent choice for content-heavy applications. You could call it somewhat a possible successor.

The php-collective/djot composer package brings full Djot support to PHP 8.2+, with 100% compatibility with the official djot test suite.

Use Cases

Let’s talk about common cases where such a markup language would be beneficial:

  • Blog engines and CMS platforms
  • Documentation systems
  • Technical writing applications
  • User-generated content (comments, forums) with Profile-based restrictions
  • Any project requiring lightweight markup with advanced formatting
  • Customizable to specific (business relevant) markup/constructs
  • Secure by design

Let’s see if Djot fits these needs.

Feature Highlights

Rich Text Formatting

Djot supports the familiar emphasis and strong formatting, plus several extras:

Syntax Result Description
*Strong* Strong Bold text
_Emphasized_ Emphasized Italic text
{=Highlighted=} Highlighted Highlighted text
{+Inserted+} Inserted Inserted text
{-Deleted-} Deleted Deleted text
`code` code Inline code
E=mc^2^ E=mc2 Superscript
H~2~O H2O Subscript

Smart Typography

Smart quotes, em-dashes, en-dashes, and ellipsis are handled automatically:

  • "Hello" becomes “Hello” with curved quotes
  • --- becomes an em-dash (—)
  • -- becomes an en-dash (–)
  • ... becomes an ellipsis (…)

Tables with Alignment

Full table support with column alignment:

| Feature     | Status |   Notes |
|:------------|:------:|--------:|
| Left-align  | Center | Right   |

Task Lists

Native checkbox support for task lists:

- [x] Create parser
- [x] Create renderer
- [ ] World domination

Since this post is written in Djot, here’s the actual rendered output:

  • Create parser
  • Create renderer
  • World domination

Divs with Classes

Create styled containers with the triple-colon syntax:

::: warning
This is a warning message.
:::

Renders as:

<div class="warning">
<p>This is a warning message.</p>
</div>

Live demo:

Note: This is a note block. Use it for tips, hints, or additional information that complements the main content.

Warning: This is a warning block. Use it to highlight important cautions or potential issues that readers should be aware of.

Spans with Attributes

Add classes, IDs, or custom attributes to inline content:

This is [important]{.highlight #key-point}

Code Blocks

Fenced code blocks with syntax highlighting hints:

```php
$converter = new DjotConverter();
echo $converter->convert($text);
```

Captions (Images, Blockquotes & Tables)

The ^ prefix adds a caption to the block immediately above it:

Block Type HTML Output
Image <figure> + <figcaption>
Table <caption> inside <table>
Blockquote <figure> + <figcaption>
> To be or not to be,
> that is the question.

^ William Shakespeare

Renders as:

To be or not to be, that is the question.

William Shakespeare

The Markdown Elephant in the Room

Let’s be honest: Markdown has quirks. Ever spent 20 minutes debugging why your nested list won’t render correctly? Or wondered why _this_works_ but _this_doesn't_ in some parsers?

Djot was designed by someone who knows these pain points intimately – John MacFarlane literally wrote the CommonMark spec. With Djot, he started fresh with lessons learned from years of Markdown edge cases.

The result? A syntax that feels familiar but actually behaves predictably. Your users write content, not workarounds.

Why Djot Over Markdown?

  • More consistent syntax – Fewer edge cases and ambiguities
  • Better nesting – Clear rules for nested emphasis and containers
  • Built-in features – Highlights, insertions, deletions, and spans without extensions
  • Smart typography – Automatic without additional plugins
  • Cleaner specification – Easier to implement correctly
  • Easier to extend – AST makes adding new features straightforward
  • Secure by design – Random unfenced HTML like <b>...</b> shouldn’t be treated as such blindly

Djot vs Markdown: Quick Comparison

Feature Markdown Djot
Strong **text** or __text__ *text*
Emphasis *text* or _text_ _text_
Highlight ❌ (needs extension) {=text=}
Insert/Delete ❌ (needs extension) {+text+} / {-text-}
Attributes ❌ (non-standard) [text]{.class #id}
Divs ❌ ::: classname
Smart quotes Depends on parser Always on
Nested emphasis Inconsistent Predictable
Hard line breaks Two trailing spaces Visible \ (backslash)

Trailing spaces are problematic since most IDEs and editors auto-trim whitespace. Using a visible \ character is much cleaner.

Auto-HTML is also problematic for user-generated content. Djot treats everything as text by default – you must explicitly enable raw HTML (see below).

Basic Usage

Converting Djot to HTML is straightforward:

use Djot\DjotConverter;

$converter = new DjotConverter();
$html = $converter->convert($djotText);

Need XHTML output? Just pass a flag:

$converter = new DjotConverter(xhtml: true);

Advanced Usage

For more control, you can work with the AST directly:

$converter = new DjotConverter();

// Parse to AST
$document = $converter->parse($djotText);

// Manipulate the AST if needed...

// Render to HTML
$html = $converter->render($document);

Markdown compatibility modes

Note: This is specific to this library and not yet officially in the specs. Using this in your apps means, your users get the best out of both concepts, but it also means you need to clarify and document this and cannot “just” link to djot specs.

Soft break mode

Configure soft breaks as per context and user needs:

Mode HTML Output Browser Display
Newline \n No visible break (whitespace collapsed)
Space No visible break (whitespace collapsed)
Break <br> Visible line break
$renderer = $converter->getRenderer(); // HtmlRenderer

// Default - newline in source, invisible in browser
$renderer->setSoftBreakMode(SoftBreakMode::Newline);

// Space - same visual result, slightly smaller HTML
$renderer->setSoftBreakMode(SoftBreakMode::Space);

// Break - every source line break becomes visible <br>
$renderer->setSoftBreakMode(SoftBreakMode::Break);

This actually allows a certain compatibility with users that are used to Markdown line breaking within normal text. So this is useful for chats or simple text inputs.

As this only affects the rendering, but not the parsing, this is still fully spec-compliant in that way.

Significant Newlines Mode (Markdown-Like)

This mode is for users accustomed to Markdown’s “human” behavior where newlines intuitively interrupt blocks.

The Djot specification states: “Paragraphs can never be interrupted by other block-level elements.”

In standard Djot, this means lists and other elements require blank lines before them – more “spaced” than what Markdown users expect.

There’s an easy solution to get the best of both worlds:

$converter = new DjotConverter(significantNewlines: true);

$result = $converter->convert("Here's a list:
- Item one
- Item two");
// Output: <p>Here's a list:</p>\n<ul><li>Item one</li><li>Item two</li></ul>

If you need a marker character (-, *, +, >) at the start of a line without triggering a block, use escaping:

// Without escaping - creates a list
$result = $converter->convert("Price:
- 10 dollars");
// Output: <p>Price:</p><ul><li>10 dollars</li></ul>

// With escaping - literal text
$result = $converter->convert("Price:
\\- 10 dollars");
// Output: <p>Price:<br>- 10 dollars</p>

This returns you to standard Djot behavior for that line.

This mode is useful when migrating existing systems where users expect Markdown-like behavior – most content works without changes, and the rare edge cases can be escaped. For offline docs and anything needed to be more agnostic one should still use the default spec compliant way.

Customization

Extension System

The library includes a clean, modern extension system, making common features trivial to add:

use Djot\DjotConverter;
use Djot\Extension\ExternalLinksExtension;
use Djot\Extension\TableOfContentsExtension;
use Djot\Extension\DefaultAttributesExtension;

$converter = new DjotConverter();
$converter
    ->addExtension(new ExternalLinksExtension())
    ->addExtension(new TableOfContentsExtension(position: 'top'))
    ->addExtension(new DefaultAttributesExtension([
        'image' => ['loading' => 'lazy'],
        'table' => ['class' => 'table table-striped'],
    ]));

Built-in Extensions

Extension Description
AutolinkExtension Auto-links bare URLs and email addresses
DefaultAttributesExtension Adds default attributes by element type (lazy loading, CSS classes)
ExternalLinksExtension Adds target="_blank" and rel="noopener noreferrer" to external links
HeadingPermalinksExtension Adds clickable anchor links () to headings
MentionsExtension Converts @username patterns to profile links
TableOfContentsExtension Generates TOC from headings with optional auto-insertion

The DefaultAttributesExtension is particularly useful:

$converter->addExtension(new DefaultAttributesExtension([
    'image' => ['loading' => 'lazy', 'decoding' => 'async'],
    'table' => ['class' => 'table table-bordered'],
    'block_quote' => ['class' => 'blockquote'],
]));

Extensions can also be combined. For example, AutolinkExtension should be registered before ExternalLinksExtension so auto-linked URLs also get the external link attributes.

Custom Rendering with Events

For more control, use the event system directly:

use Djot\Renderer\Event\RenderEvent;

$renderer = $converter->getRenderer();

// Convert :emoji: symbols to actual emoji
$renderer->addEventListener('render.symbol', function (RenderEvent $event) {
    $node = $event->getNode();
    $emoji = match ($node->getName()) {
        'smile' => '😊',
        'heart' => '❤',
        'rocket' => '🚀',
        default => ':' . $node->getName() . ':',
    };
    $event->setHtml($emoji);
});

// Add target="_blank" to external links
$renderer->addEventListener('render.link', function (RenderEvent $event) {
    $link = $event->getNode();
    $url = $link->getDestination();
    if (str_starts_with($url, 'http')) {
        $link->setAttribute('target', '_blank');
        $link->setAttribute('rel', 'noopener noreferrer');
    }
});

Custom Inline Patterns

Need #hashtags or wiki-style links? The parser supports custom inline patterns:

use Djot\Node\Inline\Link;
use Djot\Node\Inline\Text;

$parser = $converter->getParser()->getInlineParser();

// #hashtags → tag pages
$parser->addInlinePattern('/#([a-zA-Z][a-zA-Z0-9_]*)/', function ($match, $groups) {
    $link = new Link('/tags/' . strtolower($groups[1]));
    $link->appendChild(new Text('#' . $groups[1]));
    return $link;
});

echo $converter->convert('Check out #PHP and #Djot!');
// <p>Check out <a href="/tags/php">#PHP</a> and <a href="/tags/djot">#Djot</a>!</p>

Custom block patterns are also supported for admonitions, tab containers, and more. See the Cookbook for recipes including wiki links, math rendering, and image processing.

Feature Restriction: Profiles

SafeMode prevents XSS attacks, but what about controlling which markup features users can access? A comment section probably shouldn’t allow headings, tables, or raw HTML – not because they’re dangerous, but because they’re inappropriate for that context.

That’s where Profiles come in. They complement SafeMode by restricting available features based on context:

use Djot\Profile;

// Comment sections: basic formatting only
$converter = new DjotConverter(profile: Profile::comment());

// Blog posts: rich formatting, but no raw HTML
$converter = new DjotConverter(profile: Profile::article());

// Chat messages: text, bold, italic - that's it
$converter = new DjotConverter(profile: Profile::minimal());

SafeMode vs Profile

Concern SafeMode Profile
Purpose Security (XSS prevention) Feature restriction
Blocks javascript: URLs, event handlers Headings, tables, raw HTML
Target Malicious input Inappropriate formatting

Use both together for user-generated content:

$converter = new DjotConverter(
    safeMode: true,
    profile: Profile::comment()
);

Built-in Profiles

Each profile is designed for specific use cases:

  • Profile::full() – Everything enabled (admin/trusted content)
  • Profile::article() – Blog posts: no raw HTML, allows headings/tables
  • Profile::comment() – User comments: no headings/tables, adds rel="nofollow ugc" to links
  • Profile::minimal() – Chat: text, bold, italic only

Understanding Restrictions

Profiles can explain why features are restricted:

$profile = Profile::comment();
echo $profile->getReasonDisallowed('heading');
// "Headings would disrupt page hierarchy in user comments"

echo $profile->getReasonDisallowed('raw_block');
// "Raw HTML could bypass template styling and security measures"

Link Policies

Control where users can link to:

use Djot\LinkPolicy;

// Only allow links to your own domain
$profile = Profile::comment()
    ->setLinkPolicy(LinkPolicy::internalOnly());

// Or whitelist specific domains
$profile = Profile::comment()
    ->setLinkPolicy(
        LinkPolicy::allowlist(['docs.php.net', 'github.com'])
            ->withRelAttributes(['nofollow', 'ugc'])
    );

Graceful Degradation

When users try restricted features, content converts to plain text by default – nothing is lost:

$converter = new DjotConverter(profile: Profile::minimal());
$html = $converter->convert('# Heading attempt');
// Renders: <p>Heading attempt</p> (text preserved, heading stripped)

For stricter handling, you can strip content entirely or throw exceptions:

$profile = Profile::minimal()->setDefaultAction(Profile::ACTION_STRIP);
// Or for APIs:
$profile = Profile::minimal()->setDefaultAction(Profile::ACTION_ERROR);

Architecture

The package uses a clean separation of concerns:

  • BlockParser – Parses block-level elements (headings, lists, tables, code blocks, etc.)
  • InlineParser – Processes inline elements within blocks (emphasis, links, code spans)
  • HtmlRenderer – Converts the AST to HTML output

This AST-based approach makes the codebase maintainable and opens possibilities for alternative output formats.

There are also other compatibility renderers available, as well as converters to convert existing markup to Djot.

WordPress Plugin: Djot Markup for WP

Want to use Djot in your WordPress site? There’s now a dedicated plugin that brings full Djot support to WordPress.

Features

  • Full Content Processing – Write entire posts in Djot syntax
  • Shortcode Support – Use [djot]...[/djot] for mixed content
  • Syntax Highlighting – Built-in highlight.js with 12+ themes
  • Profiles – Limit functionality per post/page/comment type, disable raw HTML
  • Admin Settings – Easy configuration via Settings → WP Djot
  • Markdown compatibility mode and soft-break settings if coming from MD

Fun fact: I just migrated this blog from custom markdown-hacks to Djot (and wrote this post with it). For that I used the built in migrator of that WP plugin as well as a bit of custom migration tooling.

I needed to migrate posts, articles and comments – all in all quite straightforward though. The new interface with quick markdown-paste and other useful gimmicks helps to speed up technical blogging actually. It is both safe (comments use the right profile) and reliable.

The plugin also comes with useful semantic customization right away:

Djot Syntax HTML Output Output Use Case
[CSS]{abbr="Cascading Style Sheets"} <abbr title="...">CSS</abbr> CSS Abbreviations
[Ctrl+C]{kbd=""} <kbd>Ctrl+C</kbd> Ctrl+C Keyboard input
[term]{dfn=""} <dfn>term</dfn> term Definition term

On top, it has some gotchas as extensions:

  • ![Alt text](https://www.youtube.com/watch?v=aVx-zJPEF2c){video} renders videos from all WP supported sources right away, customize the attributes as always: {video width=300 height=200}
  • Import from HTML or markdown

You can extend the customizations also on your own.

IDE Support: IntelliJ Plugin

For developers using PhpStorm, IntelliJ IDEA, or other JetBrains IDEs, there’s now an official Djot plugin available.

Features

  • Syntax Highlighting – Full TextMate grammar support for .djot files
  • Live Preview – Split-view editor with real-time rendered output
  • Theme Sync – Preview follows your IDE’s dark/light mode
  • Code Block Highlighting – Syntax highlighting within fenced code blocks
  • HTML Export – Save documents as rendered HTML files
  • Live Templates – Code snippets for common Djot patterns

The plugin requires JetBrains IDE 2024.1+ and Java 17+.

Enhancements

The library and the WP plugin already have some useful enhancements beyond the spec:

  • Full attribute support
  • Boolean Attribute Shorthand
  • Fenced Comment Blocks
  • Multiple Definition Terms and Definition Descriptions
  • Abbreviations definitions
  • Captions for Images, Tables, and Block Quotes
  • Markdown compatibility mode (Significant Newlines)

These extend beyond the current spec but are documented as such. Keep this in mind if you need cross-application compatibility.

There is a highlight.js extension available to also code highlight djot content.

Performance

How fast is it? We benchmarked djot-php against Djot implementations in other languages:

Implementation ~56 KB Doc Throughput vs PHP
Rust (jotdown) ~1-2 ms ~30+ MB/s ~10x faster
Go (godjot) ~2-4 ms ~15+ MB/s ~5x faster
JS (@djot/djot) ~8 ms ~7 MB/s ~2x faster
PHP (djot-php) ~18 ms ~3 MB/s baseline
Python (markdown-it) ~37 ms ~1.5 MB/s ~2x slower*

*Python comparison uses Markdown parsers since no Djot implementation exists for Python.

Key observations: – PHP processes ~2-3 MB/s of Djot content consistently – Performance scales linearly O(n) with document size – Safe mode and Profiles have negligible performance impact – Comparable to Python, ~2x slower than JavaScript reference implementation

For typical blog posts and comments (1-10 KB), parsing takes under 5 ms. A 1 MB document converts in ~530 ms using ~44 MB RAM.

The performance documentation includes detailed benchmarks, memory profiling, and stress test results.

Comparison with PHP Markup Libraries

It is also interesting to compare it with other PHP parsers, usually markdown obviously:

Library 27KB Doc Throughput vs djot-php
erusev/parsedown 1.73 ms 15.6 MB/s 5.9x faster
michelf/php-markdown 5.26 ms 5.1 MB/s 1.9x faster
michelf/php-markdown (Extra) 6.12 ms 4.4 MB/s 1.7x faster
djot-php 10.22 ms 2.6 MB/s baseline
league/commonmark 16.17 ms 1.7 MB/s 1.6x slower
league/commonmark (GFM) 16.86 ms 1.6 MB/s 1.7x slower

No surprise:

  1. Simpler architecture – Parsedown uses a single-pass regex-based approach without building a full AST (Abstract Syntax Tree). It directly outputs HTML while parsing.
  2. No AST overhead – djot-php and CommonMark build a complete node tree first, then traverse it to render. This two-phase approach enables features (events, transforms, multiple output formats) but costs time.

Key finding with equivalent features enabled:

Library Time vs djot-php
djot-php 11.36 ms baseline
CommonMark (GFM) 15.00 ms 1.3x slower
CommonMark (Full) 23.54 ms 2.1x slower

Djot syntax was designed for efficient parsing

Feature Comparison

Feature djot-php CommonMark Parsedown Michelf
Basic formatting Yes Yes Yes Yes
Tables Yes GFM only Yes Extra
Footnotes Yes No No Extra
Definition lists Yes No No Extra
Task lists Yes GFM only No No
Smart typography Yes No No No
Math expressions Yes No No No
Attributes Yes No No Extra
Highlight/Insert/Delete Yes No No No
Super/Subscript Yes No No No
Divs/Sections Yes No No No
Event system Yes Yes No No
Safe mode Yes Yes Yes Yes
Profiles Yes No No No
Extension system Yes Yes No No

Features Unique to djot-php

  1. Smart Typography – Automatic curly quotes, em/en dashes, ellipsis
  2. Math Expressions – Inline $x^2$ and display $$ math
  3. Highlight/Insert/Delete{=highlight=}, {+insert+}, {-delete-}
  4. Super/SubscriptH~2~O, x^2^
  5. Divs with Classes::: warning ... :::
  6. Profile System – Restrict features per context (full/article/comment/minimal)
  7. Abbreviations – Auto-wrap terms with <abbr> tags
  8. Captions – For images, tables, and blockquotes
  9. Converters – Import from Markdown, BBCode, HTML

Importing and Migration

You can often with a boolean flag just continue to support the current markup, and with new content add djot based content. For those that want to migrate, there is some built in tooling and converters:

  • HtmlToDjot
  • MarkdownToDjot
  • BbcodeToDjot

Fun fact: They also serve as a nice round-trip validation, to check if the transformation from and to is loss-free. Send a doc into it and reverse it, and the content should still “match” without loss of supported structures.

What’s Next?

The library is actively maintained with plans for:

  • Additional renderers (convert Djot back for interoperability)
  • More converters
  • More markup supported (not contradicting the specs)
  • Maybe some framework specific plugins or integrations

Contributions welcome!

Some personal notes

I would have liked URLs and images to have a bit more friendly syntax as well, e.g. [link: url "text"] style for links and [image: src "alt"] style for images. The ![](url) style still feels a bit too much like code syntax to me.

If I were ever to invent a new markup language, I would probably take a similar approach, but try to keep it even simpler by default. The {} braces seem a bit heavy for these common use cases, and for non-technical users.

One of the quirks I had to get used to, was the automated flow (line breaks are ignored) and the need for the visible (hard) line break if really desired. But in the end it usually helps to keep clear paragraphs. And I added compatibility options as opt-in for upgrading or usability ease.

Overall, Djot strikes a great balance between familiarity and consistency. And at least topics like URL/image can be easily added as extension if desired.

The PHP implementation with djot-php library is the most complete implementation of the standard available. It is perfectly suited for web-based usage. Make sure to check out the live sandbox and play around with the complex examples!

Links

Give Djot PHP a try in your next project. The familiar syntax with improved consistency and a lot more out of the box might just win you over.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

CakePHP File Management Solution 27 Nov 5:20 AM (17 days ago)

Meet the New FileStorage Plugin

A Modern File Storage Solution for CakePHP.

Remember when handling file uploads in PHP meant wrestling with $_FILES, manually moving uploaded files, hardcoding paths throughout your application, and hoping nothing breaks when you need to move files to S3? We’ve all been there.

For years, CakePHP developers have cobbled together various solutions—some rolled their own upload handlers, others patched together different libraries, and the brave ones tried to keep legacy plugins alive across framework versions. But let’s face it: file management is harder than it looks. You need thumbnails? Add another library. Want to store in the cloud? Rewrite your code. Need to track metadata? Better add more database columns everywhere.

Those days are over.

Enter: The Modern FileStorage Plugin

There’s a new player on the CakePHP block, and it’s bringing modern architecture, clean abstractions, and production-ready code to solve file management once and for all. Built from the ground up for CakePHP 5.x with PHP 8.1+, the FileStorage plugin isn’t just another upload library—it’s a complete storage solution that finally gets file handling right.

Think of it as the file management system you wish you’d had on your last three projects.

What Makes It Different?

Store Anywhere (Seriously, Anywhere)

Here’s where it gets interesting. The plugin is built on FlySystem v3, which means your files can live anywhere—and I mean anywhere. Local disk today, AWS S3 tomorrow, Azure next month? Just change your config. Your application code? Stays exactly the same.

Start developing locally, deploy to S3 in production, switch to Azure when the business changes cloud providers next year. The plugin doesn’t care, and neither does your code. That’s the power of proper abstraction.

Supported out of the box:

  • Local filesystem (the classic)

  • Amazon S3 (the popular choice)

  • Azure Blob Storage (the enterprise favorite)

  • FTP/SFTP (for that legacy server)

  • Dropbox (why not?)

  • In-memory storage (perfect for testing)

  • Any other backend via FlySystem adapters

One interface. Any storage. Zero refactoring.

Set It and Forget It

Remember writing upload handling code in every controller action? Yeah, me too. Not anymore.

Attach the FileStorageBehavior to your model, and watch the magic happen. Files upload when you save. Files delete when you remove entities. It just works, like it should have from the beginning:

// In your Table class - that's it!
public function initialize(array $config): void
{
    parent::initialize($config);
    
    $this->addBehavior('FileStorage.FileStorage', [
        'fileField' => 'file',
    ]);
}

Your controllers stay clean. Your code stays simple. Your future self says thank you.

Smart Architecture: One Table to Rule Them All

Here’s something clever: instead of adding avatar_path, document_path, and thumbnail_path columns to every table in your database (we’ve all done it), the plugin uses a single file_storage table for all file metadata.

Think about it. Your users table doesn’t need file paths. Your products table doesn’t need file paths. They just have relationships to the FileStorage model, like any other association in CakePHP.

What you get:

  • Clean schema – No path strings cluttering your business data

  • Easy migrations – Move files around without touching your core tables

  • Perfect audit trail – Every upload tracked with metadata, hashes, timestamps

  • Duplicate detection – File hashes catch duplicate uploads automatically

  • Flexible relationships – One file, many owners? No problem.

It’s the architecture you’d design if you had time to think it through properly. Good news: someone already did.

Image Processing That Actually Works

“Just generate a thumbnail” said the client, as if it’s simple. Except now you need three sizes. And they want them optimized. And cropped to exact dimensions. And…

Stop. Take a breath. The plugin’s got you covered.

With the optional php-collective/file-storage-image-processor package (powered by Intervention Image v3), you can generate any variant you need with a fluent, chainable API that actually makes sense:

$variants = ImageVariantCollection::create()
    ->addNew('thumbnail')
        ->scale(300, 300)
        ->optimize()
    ->addNew('medium')
        ->scale(800, 600)
        ->sharpen(10)
        ->optimize()
    ->addNew('profile')
        ->cover(150, 150)
        ->optimize();

$imageProcessor->process($file, $variants);

Upload one image, get four versions (original + three variants), all optimized, all stored, all tracked. One line of code to process them all.

The full toolkit:

  • scale – Maintain aspect ratio (the one you actually want most of the time)

  • resize – Exact dimensions (when you really mean it)

  • cover – Smart zoom-crop (perfect for profile pics)

  • crop – Surgical precision extracts

  • rotate – Because users upload sideways photos

  • flip operations – Mirror, mirror on the wall

  • sharpen – Make those photos pop

  • optimize – Smaller files, same quality

  • callback – Your custom wizardry

Chain them, combine them, go wild. The API won’t judge.

The Details That Matter

Organized Storage, Your Way

The path builder keeps things tidy automatically:

// Generated paths like:
{model}/{collection}/{randomPath}/{id}/{filename}.{extension}
{model}/{collection}/{randomPath}/{id}/{filename}.{hashedVariant}.{extension}

// Example:
// Posts/Cover/a1/b2c3d4e5/my-image.jpg
// Posts/Cover/a1/b2c3d4e5/my-image.abc123.jpg (variant)

Organize by model, by collection, by random path levels—whatever makes sense for your app. The plugin handles the patterns.

Validation Built In

Because nobody wants 500MB BMPs crashing their server:

  • File type and extension checks

  • Size limits that actually work

  • MIME type validation

  • Image dimension constraints

  • Full PSR-7 support

Set your rules once, enforce them everywhere.

Built on Solid Foundations

This isn’t a weekend hackathon project. The plugin is architected with production in mind:

Event-driven design means you can hook into any file operation—send notifications, trigger processing, update CDNs, whatever you need.

Dependency injection throughout makes testing actually pleasant and customization straightforward.

Standards-based on FlySystem, Intervention Image, PSR-7, and CakePHP best practices. No weird custom abstractions to learn.

Quality assured with PHPStan level 8, comprehensive tests, and active maintenance.

What Can You Build?

E-Commerce Done Right

Your client uploads one massive 5000x5000px product photo. The plugin generates thumbnail, gallery, and zoom versions, optimizes them all, and stores them wherever you want:

$variants = ImageVariantCollection::create()
    ->addNew('thumbnail')->scale(150, 150)->optimize()
    ->addNew('gallery')->scale(600, 600)->optimize()
    ->addNew('zoom')->scale(1200, 1200)->optimize();

Your customers get fast page loads. Your S3 bill stays reasonable. Everybody wins.

Social Platforms with Style

User avatars that actually look good everywhere—navbar, profile page, comment sections, notification icons:

$variants = ImageVariantCollection::create()
    ->addNew('avatar')->cover(200, 200)->optimize()
    ->addNew('avatar_small')->cover(50, 50)->optimize();

Smart cropping means faces stay centered. Automatic optimization means your CDN costs don’t explode.

Document Management That Scales

PDFs with thumbnail previews. File metadata tracked perfectly. Full audit trails. Version history. And when your startup grows up and needs to move everything to cloud storage? Update the config. Done.

Multi-Tenant Applications

Different storage backend per customer? Different path structures per tenant? Clean data separation? The plugin’s architecture makes it straightforward instead of scary.

Beyond the Basics: Extension Points

Here’s where it gets exciting. The plugin’s architecture isn’t just solid—it’s designed for extension. The event-driven design, flexible metadata system, and clean abstractions mean you can build some impressive features on top.

Already Possible (Right Now)

Asynchronous variant generation

Don’t handle all rendering synchronously, but render only mini-preview, and let the rest be done via background queue.

class QueuedFileStorageBehavior extends FileStorageBehavior
{
    /**
     * Default configuration
     *
     * @var array<string, mixed>
     */
    protected array $_defaultConfig = [
        'queueVariants' => true,  // Auto-queue by default
        'immediateVariants' => [], // Generate these immediately
        'queuedVariants' => [],    // Queue these for background
    ];
    
    ...
}

Secure File Serving with Authorization Here’s something most upload plugins ignore: access control. The FileStorage plugin doesn’t just store files—it provides utilities for serving them securely.

The plugin gives you URL generation and signed URL helpers, but intentionally doesn’t include a one-size-fits-all serving controller. Why? Because your authorization logic is yours. The plugin provides the tools; you implement the rules that make sense for your application.

use FileStorage\Utility\SignedUrlGenerator;

$signatureData = SignedUrlGenerator::generate($entity, ['expires' => strtotime('+1 hour')]);

// Or implement your own serving controller with custom authorization:
// - Ownership-based (only file owner can access)
// - Role-based (admins see everything, users see their department)
// - Related entity access (file visible if parent album is visible)
// - Time-based (files available only during business hours)
// - Combination of above

The FileServing documentation provides complete examples for each pattern. You get the security infrastructure without the opinionated authorization that never quite fits your needs.

Custom Metadata & Advanced Search The centralized storage table includes metadata fields you can extend. Add tags, categories, descriptions, or any custom data. Then search, filter, and organize files however your application needs:

$entity = $fileStorage->newEntity([
    'file' => $uploadedFile,
    'metadata' => [
        'tags' => ['product', 'winter-2024'],
        'department' => 'Marketing',
        'project_code' => 'PRJ-123',
    ],
]);

File Versioning Want to track file versions? The architecture supports it naturally through the metadata and collection fields. Store multiple versions with relationships, track upload timestamps, and let users restore previous versions. The collections system makes it clean:

// Store new version, link to original via metadata
$newVersion = $fileStorage->newEntity([
    'file' => $newFile,
    'model' => 'Documents',
    'collection' => 'versions',
    'foreign_key' => $document->id,
    'metadata' => [
        'original_file_id' => $originalFile->id,
        'version' => 2,
    ],
]);

Community-Driven Roadmap

The plugin has an active roadmap shaped by real-world usage. Here’s what’s being discussed and built:

Smart Image Optimization – Auto-format selection (WebP when supported), responsive image sets for different screen sizes, automatic quality adjustment based on file size targets.

Bulk Operations – Select and process multiple files at once. Batch tagging, bulk downloads as ZIP, mass deletions, metadata updates across hundreds of files.

Storage Analytics – Dashboard showing storage usage by type, upload trends, file age distribution, and storage costs. Great for resource planning and quota management.

CDN Integration Helpers – Simplified configuration for CloudFront, CloudFlare, and other CDNs. Automatic cache invalidation, signed URL generation, geo-routing support.

Advanced Transformations – Watermarking pipelines, image filter presets (vintage, black-and-white, etc.), format conversion workflows, PDF thumbnail generation.

Access Control Layers – Permission checks before downloads, user-specific file visibility, role-based access to collections, audit logging for compliance.

Client-Side Widgets – Drop-in components for drag-and-drop uploads with progress bars, inline image cropping, preview before upload, chunked uploads for large files.

What This Means for You

You’re not locked into version 4.0’s feature set. The architecture is built to grow with your needs:

  • Start simple – Basic uploads and storage

  • Add complexity gradually – Implement versioning when you need it

  • Customize freely – The event system lets you hook into everything

  • Stay updated – Active development means new features land regularly

The plugin isn’t just solving today’s file storage problems—it’s built to handle tomorrow’s requirements too.

Getting Started (It’s Easier Than You Think)

Install It

composer require dereuromark/cakephp-file-storage

Want image processing too?

composer require php-collective/file-storage-image-processor

Configure It

Load the plugin:

// src/Application.php
public function bootstrap(): void
{
    parent::bootstrap();
    $this->addPlugin('FileStorage');
}

Run migrations (creates the file_storage table):

bin/cake migrations migrate -p FileStorage

Configure your storage programmatically (typically in bootstrap or a dedicated config file):

// config/storage.php or config/bootstrap.php
use PhpCollective\Infrastructure\Storage\StorageAdapterFactory;
use PhpCollective\Infrastructure\Storage\StorageService;
use PhpCollective\Infrastructure\Storage\Factories\LocalFactory;
use PhpCollective\Infrastructure\Storage\FileStorage;
use PhpCollective\Infrastructure\Storage\PathBuilder\PathBuilder;

$storageFactory = new StorageAdapterFactory();
$storageService = new StorageService($storageFactory);
$storageService->addAdapterConfig('Local', LocalFactory::class, [
    'root' => WWW_ROOT . 'files' . DS,
]);

$pathBuilder = new PathBuilder();
$fileStorage = new FileStorage($storageService, $pathBuilder);

Configure::write('FileStorage.behaviorConfig', [
    'fileStorage' => $fileStorage,
]);

Use It

// In your controller - that's the whole thing
public function upload()
{
    if ($this->request->is('post')) {
        $file = $this->request->getData('file');
    
        $fileStorage = $this->fetchTable('FileStorage.FileStorage');
        $entity = $fileStorage->newEntity([
            'file' => $file,
            'model' => 'Products',
            'foreign_key' => $product->id,
        ]);
    
        if ($fileStorage->save($entity)) {
            $this->Flash->success('File uploaded successfully');
        }
    }
}

That’s it. No hidden steps. No weird gotchas. It just works.

Why This Plugin Exists

Let’s be honest: file management is one of those problems that sounds simple until you actually try to solve it properly. Then it becomes a mess of edge cases, path handling, storage migrations, and “we need thumbnails now” feature requests.

The FileStorage plugin exists because developers kept solving the same problems over and over, poorly, under deadline pressure. Someone finally said “enough” and built the solution we all wish we’d had from the start. Burzum (Florian) started this project, and I just happened to finish it up, test it with real apps, and publish it for everyone to use.

What You Get

Production-grade code – PHPStan level 8, comprehensive tests, real-world battle-testing. This isn’t beta software.

Flexible from day one – Start local, scale to cloud, switch providers. The architecture doesn’t care where your files live.

Time back in your life – Stop reinventing uploads. Stop debugging path concatenation. Stop explaining to clients why migrating storage is hard.

Community-proven – Built by CakePHP veterans, used in production apps, improved by real-world feedback.

The Modern Stack: Version 4.0.0

This isn’t a dusty old plugin getting by on legacy code. Version 4.0 brings modern everything:

  • CakePHP 5.x native – Built for the latest framework

  • FlySystem v3 – The best storage abstraction available

  • Stable dependencies – Everything’s at 1.0.0+

  • Intervention Image v3 – Latest image processing with improved performance

Fresh code. Modern patterns. Zero compromises.

See It In Action

Words are cheap. Code is proof.

The live demo shows real uploads, real image processing, real variant generation. Upload a file, watch it generate thumbnails, see how the API works. No registration, no signup forms, just working code you can play with.

Try the live demo: https://sandbox.dereuromark.de/sandbox/file-storage-examples

Want the technical details, changelog, and upgrade notes?

Release notes: https://github.com/dereuromark/cakephp-file-storage/releases/tag/4.0.0

Not Just for CakePHP

Here’s a bonus: the core libraries powering this plugin aren’t tied to CakePHP. They’re framework-agnostic PHP packages that work in any project.

The plugin itself (dereuromark/cakephp-file-storage) is a convenient CakePHP wrapper that adds behaviors, helpers, and framework integration. But under the hood, it’s built on:

  • php-collective/file-storage – The core storage abstraction and file management

  • php-collective/file-storage-factories – Simplified FlySystem adapter configuration

  • php-collective/file-storage-image-processor – Image processing and variant generation

All three packages work standalone in any framework, or plain PHP projects. They use standard PSR interfaces, Dependency Injection, and have zero framework dependencies.

What this means:

If you’re building a CakePHP application, use the plugin. You get behaviors, table integration, migrations, and everything configured to work with CakePHP conventions out of the box.

If you’re working with another framework, use the underlying libraries directly. Same powerful file storage and image processing, just without the CakePHP sugar coating.

If you’re migrating between frameworks, your file storage logic can stay the same. Only the wrapper layer changes.

The architecture is portable. The investment is protected. Build once, use anywhere.

The Bottom Line

File storage is a solved problem now. You don’t need to solve it again.

Whether you’re building a simple blog with avatars, a document management system, an e-commerce platform, or the next big SaaS app—the FileStorage plugin gives you production-ready file handling that actually scales.

Install it. Configure it. Forget about it. Move on to building features that actually matter.

Welcome to modern file storage for CakePHP.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

TestHelper for your CakePHP apps 13 Nov 11:48 PM (last month)

More than 5 years ago I mentioned it as a side topic. Now I want to showcase it with fresh look and feel and lots of more useful tooling.

The TestHelper plugin

It now ships with its own default standalone template. That makes it easier to “work” and “look correctly”, as the app itself is less likely to conflict here.

img

Cake Linter

A super useful tool you can opt-in for your CakePHP apps to make sure they are up to date and use best practice approaches. Use the built in tools, and also add your own tasks on top.

img

Add it to your CI as

bin/cake linter

You can of course also run it on your plugins:

bin/cake linter -p all

For some tasks auto-fixing is possible, in that case run it locally to auto-fix those issues found:

bin/cake linter --fix

Browser test runner

You recall the 2.x days? Yes, there was a browser-based (click and run) test runner included. It lives on in this plugin.

Why is this a good thing? In many cases clicking a link can be much quicker than manually typing or copy pasting the whole path to the test file. It also directly links the coverage to that file.

But even if you want to keep the CLI for testing, the GUI offers a few more things:

  • See all CakePHP types as overview (controller, components, tables, helpers, forms, mailers, …) and which of those have test cases and which don’t.

  • With a simple click bake the missing test cases directly from that backend. You read that right: It also bakes from the GUI.

tests

Reverse URL Lookup

A handy tool for e.g.

  • $this->redirect()

  • HtmlHelper::link() and UrlHelper::build() usage

  • $this->Form->postLink()

  • $this->get() and $htis->post() in tests

etc. All of those should get an array instead of plain strings for compatibility.

img

Compare models, DB tables and fixture (factories)

You can also bake missing fixture factories per click from the backend.

Migration Re-Do tool

Provides a multi-step process to ensure a fresh migration file replacing your current ones (merging all together). Works on a tmp-DB ton confirm the result as diff in the end.

Summary

The browser based addons can help for test-driven development, the CLI tools for asserting high quality. Check out the plugin now.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

CakePHP Tips Autumn 2025 28 Oct 6:50 PM (last month)

Healthchecks / Uptime

With increasing complexity of apps and more and more requirements on your server and tooling, it can be easily overlooked, when doing updates or migrating servers. I highly recommend a way to track your requirements.

Some are easier verified, e.g. extensions, as composer would already complain on “install”. Others like memcache or third party tools could be overlooked until maybe someone notices eventually.

My recommendation: Use Setup plugin and its healthchecks to run on changes to your server, but also in general periodically to see if all is still up and well.

Often enough somehow the postmaxsize or uploadmaxsize got reset or forgotten for either CLI or web, and suddenly the uploads didn’t work anymore for users (default often is ridiculous 2MB). Or someone forget the composer install on the server, and the version in the composer file did not match (yet) the lock file. All of those things can be checked as well, and most of those ship out of the box here as convenient checks.

Web CLI
Healthcheck Healthcheck CLI

You can define your own custom checks on top, or adjust/replace the default ones.

See docs for details.

Audit log your tables

If you ever accidentally deleted some data, or even modified it and you wanted to look what the old value was: It is usually gone. There is an easy Cake solution to it: Audit logs.

Imagine it just silently tracks all the changes for a specific table, or even all your relevant tables. And if you wanted to restore a deleted record, or parts of it, it would be right there only 1 click away.

Storage here is usually cheap and especially if those records are important to track any change on, this comes quite easy and fast. It can be useful to go back in time and find out who changed what at what time, and maybe ask that person about details on this.

I personally like to log-rotate the ones that are not business critical. They would just be around for months or weeks maybe, depending on the record modifications per table.

Checkout the AuditStash plugin for details. It also links the live sandbox demo you can play around with.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

Fresh expert tips on CakePHP IDE and tooling best practices 9 Sep 2:40 AM (3 months ago)

You most likely have read my previous posts that go into detail regarding CakePHP and how to be most efficient in the IDE, as well as most “type safe” using PHPStan and static analyzing. See e.g. here.

In this post I want to share some more recent learnings and how I deal with this right now for the absolute best results.

Every command is usually shortcut aliased in composer scripts section. And I added an alias to my ddev env:

hooks:
    post-start:
        - exec: echo 'alias c="composer"' >> ~/.bashrc

This way I can run composer update with c u.

Running tools over modified subset

If you only touched 3-4 files, running the full tooling battery over all your code can be many minutes of unnecessary waiting time. What if we ran those only over touched files?

This obviously works best for “per file” checks, and in our case “phpcs” (code style checking and fixing). The other tools (phpstan, tests, annotations) can also miss side-effects to other unrelated files. So here it is a trade off between fast and reliable. Often, if you have CI running, it would catch the side-effects and related but untouched files anyway. So the trade-off can be worth it.

I want to show you how I manage e.g. phpcs here:

    "scripts": {
        ...
        "check": [
            "@test",
            "@stan",
            "@cs-check",
            "@annotations"
        ],
        "cs-modified": "sh cs-modified.sh",
        "cs-check": "phpcs --colors",
        "cs-fix": "phpcbf --colors",
        "stan": "phpstan analyze",
        "test": "phpunit --colors=always",
        "test-coverage": "phpdbg -qrrv vendor/bin/phpunit --coverage-html webroot/coverage/",
        "annotations": "bin/cake annotate all -r",
        "setup": "bin/cake generate code_completion && bin/cake generate phpstorm"

This is in my composer.json files of all projects.

Note the “cs-modified” script. You can run c cs-m to quickly fix CS issues in the files you touched.

On top of that you can do something similar with annotator, as well as “stan”. Only tests is a bit more tricky since the files touched on project level, might not be the same as in the tests.

Here is an example implementation:

#!/bin/bash

# Get list of modified and staged PHP files
MODIFIED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB HEAD src/ tests/ config/ plugins/ | grep -E '\.php$')

# Exit if no PHP files are modified
if [ -z "$MODIFIED_FILES" ]; then
echo "No modified PHP files found."
exit 0
fi

echo "Running Code Sniffer Fixer on modified PHP files..."
vendor/bin/phpcbf $MODIFIED_FILES

echo "Checking remaining issues..."
vendor/bin/phpcs $MODIFIED_FILES

CI run

The CI, as outlined before, runs the full battery (read only):

    - php composer.phar install --no-interaction
    - vendor/bin/phpunit
    - vendor/bin/phpstan
    - vendor/bin/phpcs
    - bin/cake annotate all -r -d --ci

The “annotate” CI availability is quite new. Especially together with PHPStan this really helps to keep your false positive “green”s to a minimum, as without PHPStan reads your “outdated” annotations and based on that gives its OK.

Having the annotations aligned with the actual code and DB fields is crucial if you want PHPStan to find potential bugs after refactoring.

Pimp up your PHPStan

The default level 7 or 8 usually silently skip the undefined vars as well as mixed types. This can lead to CI being happy but your code actually is broken. See the above blog posts for details.

So I usually install (on top of phpstan itself):

composer require --dev cakedc/cakephp-phpstan
composer require --dev rector/type-perfect
composer require --dev phpstan/phpstan-strict-rules

I recommend the following phpstan.neon setup:

includes:
	- vendor/phpstan/phpstan/conf/bleedingEdge.neon
	- vendor/cakedc/cakephp-phpstan/extension.neon
	- vendor/rector/type-perfect/config/extension.neon
	- vendor/phpstan/phpstan-strict-rules/rules.neon

parameters:
	level: 8
	paths:
		- src/
	bootstrapFiles:
		- config/bootstrap.php
	type_perfect:
		no_mixed_property: true
		no_mixed_caller: true
	treatPhpDocTypesAsCertain: false
	ignoreErrors:
		- identifier: missingType.generics
		- identifier: missingType.iterableValue
		# Can be ignored to find actual issues for now
		- identifier: typePerfect.noArrayAccessOnObject
	strictRules:
		disallowedLooseComparison: false
		booleansInConditions: false
		booleansInLoopConditions: false
		uselessCast: false
		requireParentConstructorCall: false
		disallowedBacktick: false
		disallowedEmpty: false
		disallowedImplicitArrayCreation: true # Here!
		disallowedShortTernary: false
		overwriteVariablesWithLoop: true # Also good
		closureUsesThis: false
		matchingInheritedMethodNames: false
		numericOperandsInArithmeticOperators: false # !
		strictFunctionCalls: true # Also good
		dynamicCallOnStaticMethod: true # Here
		switchConditionsMatchingType: false
		noVariableVariables: false
		strictArrayFilter: true # Also good
		illegalConstructorMethodCall: true # Also good

We mainly need disallowedImplicitArrayCreation and dynamicCallOnStaticMethod here. Many of the others are actually not always good practice or overkill. So I ignore those.

If it is a new or quite clean project, I usually also enable a few more stricter checks, e.g.

	type_perfect:
		null_over_false: true

I hope this gave a bit of an overview on current tooling enhancements for your project to maximize development speed while also making sure PHPStan finds most of the issues right away that are harder to spot and might then hit you in production environment.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

Improved Session authenticator for CakePHP 19 Jul 11:25 AM (4 months ago)

There is a new CakePHP session authenticator on the block.

Status quo

Historically, the Session provider in CakePHP 3.x times using the AuthComponent stored an array of your user (entity) in the session upon login. It was accessed using Auth.User, so Auth.User.id was the user’s ID.

With the split and adaptation of separate auth plugins and components, the data here became an Identity object, and in the session the user got stored as the User entity object itself. The session key also changed to Auth, meaning the ID would now be accessed as Auth.id, since Auth is the session key for the User entity (identity) directly. On top of that, the access is now purely through the Identity in the request object, as it could also always come from Cookie or other authenticators.

This User session now being an Entity object makes things more problematic. As now we have more objects in the session directly (serialized). Not just DateTime and (backed) enums, but also User entity and possible contained relations. If any of those change just slightly upon a deployment, whole sessions would be wiped out due to the unserialization not working out anymore. These session invalidations can, of course, be mitigated to some extent by adding Cookie auth on top.

For the last years, I stuck to my TinyAuth.Auth component. So those changes didn’t really affect me.

But when I needed to actually integrate some new apps with the plugin approach, I started to look more into it. In the process, I also rewrote the authenticators to prefer the identifier directly (normal dependency inversion), as this is also a safer approach. Using a shared IdentifierCollection seems not only overkill, but it can also be harmful if you by accident have identifier keys colliding. So best to stick to one specific Identifier (collection) per Authenticator. This has been released as v3.3.0 now.

For details on how the authenticator works, see the official docs.

toArray()/fromArray()

One idea I looked into recently was to always toArray() or json_encode() the session auth data before storing it. And restoring it upon read. From the outside the authenticator would not be any different.

This also worked actually, as this PR shows.

There were concerns that this could be an issue in edge cases, and that not all fields can be safely serialized. E.g. blob/binary data in a column because its encrypted (and gets en/decrypted by table events).

So I eventually dropped this approach.

PrimaryKeySession authenticator

With those findings in mind, I explored the idea of storing only the user id in the session and freshly building the identity from it using the DB users table data.

It is working quite nicely:

$service->loadAuthenticator('Authentication.PrimaryKeySession', [
    'identifier' => [
        'Authentication.Token', [
            'dataField' => 'key', // incoming data
            'tokenField' => 'id', // lookup for DB table
            'resolver' => 'Authentication.Orm',
        ],
    ],
    'urlChecker' => 'Authentication.CakeRouter',
    'loginUrl' => [
        'prefix' => false,
        'plugin' => false,
        'controller' => 'Users',
        'action' => 'login',
    ],
]);

This has another positive side effect: The data is now always up to date, no more issues when

  • The user edits his account data and we have to “persist changes back into session identity”
  • Any admin modifies the user’s data and they are now out of sync until a fresh login.

Custom finder

I personally always use a findActive custom finder on top, to prevent logins of not activated or blocked users:

    'identifier' => [
        'Authentication.Token', [
            'dataField' => 'key', // incoming data
            'tokenField' => 'id', // lookup for DB table
            'resolver' => [
                'className' => 'Authentication.Orm',
                'finder' => 'active',
            ],
        ],
    ],

with e.g. in UsersTable:

    public function findActive(SelectQuery $query): SelectQuery
    {
        return $query->where(['email_verified IS NOT' => null]);
    }

If you need to actually also fetch related data into the identity and contain e.g. Roles or alike, you can also wrap this as

            'finder' => 'auth',

This allows you to use both contain() and where() in the same finder.

Caching

If you use a custom auth finder that does quite a few extra joins and query, e.g.

  • User “contains” Groups, Permissions, Roles, ProfileData, …

you might want to add a Cache layer in between to mitigate the constant DB queries. Then it will fetch this larger dataset from a quick (ideally memcache or redis) cache, and only require DB lookup once the data changed and the cache got invalided.

If you use caching, you need to do the invalidation yourself. It is still much easier than having to manually rewrite the identity into the session. All you need to do is SessionCache::delete($uid);, given that you configured it using Configure. Once the cached session data cannot be found, it will just look it up in the DB again and then re-cache for the next request.

Summary

Why I recommend switching to PrimaryKeySession:

  • No more issues with deployments and changes to objects
  • Always up to date User (session) data
  • Much smaller session data storage size (across all users, especially if using DB session)

Things to look out for:

  • Invalidating the cache if you add this layer to ensure users don’t have to log out and log back in manually

The official CakePHP plugin has the base version available, if you want caching included, use the TinyAuth plugin’s “extended edition”.

Docs: github.com/dereuromark/cakephp-tinyauth/blob/master/docs/AuthenticationPlugin

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

CakePHP Tips Spring 2025 6 Jun 6:45 AM (6 months ago)

DB tools

Some useful database commands have been added to Setup plugin 3.8.0+:

bin/cake db init

This will create the (default) database, if it does not yet exist. Setting up a fresh project or local version of it, the dev tool might not do this automatically. Running this command avoids having to do this manually.

You can also do the same for the test DB using -c test option.

bin/cake db reset

This will empty all tables of that (default) database, leaving all (phinx) migration tables in place (to avoid rerunning migrations).

bin/cake db wipe

This will clear out all tables of that (default) database.

I for example sometimes need that when running tests locally, having to quickly fix up the migration file and then rerunning the tests. Since they use the already run migrations and there is no clean rollback for the tests, there is only the wipe option:

bin/cake db wipe -c test

Also sometimes the tests just got messed up somehow, then resetting quickly this way also often makes it work again.

PHP Templating

When working with templating, useful helper methods can make your life easier. A yesNo() helper method can quickly switch between check/error font icons. For e.g. green/red coloring for this yes/no on top, we can use a ok() helper method to wrap this.

echo $this->Templating->ok(
    $this->IconSnippet->yesNo($subscription->enabled), 
    $subscription->enabled,
);

Details see cakephp-templating/releases/tag/0.2.7

Useful shortcut functions

I posted about the usefulness of wrapping HTML in a value object a while back. Now, having to deal with use statements all over, and especially in templates can be cumbersome. So maybe in those cases having shortcut functions can be useful.

You can define them in your bootstrap (files):

function html(string $string): \Templating\View\HtmlStringable {
    return \Templating\View\Html::create($string);
}

Then in any template, you can use them (given that you use the Templating helpers/traits):

echo $this->Html->link(html(<b>Title<b>), ['action' => 'view', $entity->id]);

Note how you can prevent options (escape true/false) from being needed, which will also improve upgradability.

Complex search filters

Did you ever have to do a range filter (min/max), e.g. on prices in a table? The Search plugin is really powerful and flexible.

Let’s give it a go: Here we need to make one of the two a dummy one just so the value is parsed into the “args”. The other rule then does the actual filtering and then has also access to that other value.

->callback('price_max', [
    'callback' => function (SelectQuery $query, array $args, $filter) {
        return false;
    },
])
->callback('price_min', [
    'callback' => function (SelectQuery $query, array $args, $filter) {
        $min = (int)$args['price_min'];
        $max = (int)($args['price_max'] ?? null);
        if (!$min && !$max || $max < $min) {
            return false;
        }
    
        $query->where(['price >=' => $min, 'price <=' => $max]);
    
        return true;
    },
]);

I personally like to display this as a slider with two handles and two hidden form control inputs. A demo of this can be found in the sandbox.

More filter examples here.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

IdeHelper plugin on steroids 25 May 4:50 AM (6 months ago)

Pretty much 5 years ago I blogged about leveraging your IDE.

In this small update here I want to showcase a very handy new feature: Live annotation updates in your code base while you bake, develop and customize.

Note: The following requires IdeHelper 2.10+ plugin version.

(File) Watchers

Currently, once you baked code, or you modified a PHP class or view template, you need to manually run the annotator. I often use -f SomeFilter to quickly run it over relevant files.

Sometimes I forget and get reminded from PHPStan then, that there are some unknown relations or class usage.

You can set up a file watcher to run the annotation tool on every file change. This way you can have a live annotation update while coding.

Using Node + Chokidar

Install using

npm init -y
npm install chokidar --save

in your project root.

Run for example:

node vendor/dereuromark/cakephp-ide-helper/annotate-watcher.cjs

If necessary, you can also customize the paths using --path=src/,templates/, for example.

You can also copy the annotate-watcher.cjs to your app and customize it.

Since this is a cross-platform tool, this is currently the recommended approach, as it is also the most performant (only touches the files directly modified). It might miss a few related templates that are not modified but would get updates. This is the tradeoff.

Using watchexec

See github.com/watchexec/watchexec.

watchexec -e php 'bin/cake annotate all'

With this, you would usually run it over all files. Still usually quite performant.

Customization

You can pull down the cjs file to your application and modify it. Then just execute that one instead.

Shortcuts

If you dont want to type the whole command every time, or if you further customized the paths, it can be useful to make this a composer script:

// your composer.json scripts section
'watcher' => 'node vendor/dereuromark/cakephp-ide-helper/annotate-watcher.cjs ...'

and then only have to run

composer watcher

If you want to run it in the background, you can further enhance it with a 2nd stop command when needed. Then it could be as simple as

composer watcher-start
composer watcher-end

Hope this gave you some useful ideas for an even more productive CakePHP coding experience.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

eInvoicing in PHP 10 May 7:38 AM (7 months ago)

The eInvoice – or “E-Rechnung” in German terminology – is a thing. I didn’t know until recently, when I had to start implementing a process here for a PHP backend.

Context

The eInvoice should already be used by businesses as we speak. As of January 1, 2025, electronic invoicing are mandatory in the B2B sector in Germany. Starting in January 2026, all EU businesses will be required to use e-invoicing for B2B transactions. All invoices submitted between businesses must be electronic.

The legal requirements here in normal words: An invoice is electronic, if

  • it is issued, transmitted, and received in a structured electronic format, and

  • this format enables the automated and electronic processing of the invoice.

For details read EN 16931 compliance.

So you can see that there isn’t too much time left here to fix up a maybe outdated approach.

Concept

The eInvoice consists primarily of an XML file. That’s the single source of truth. While a PDF is optional, it sure helps to make it visible for the human eye.

Now to keep things simple, a PDF can actually get such an XML attached, so that it is still the “same” single PDF, but now contains as payload the eInvoice information standardized to be processed by tooling.

The XML is using the CrossIndustryInvoice (CII) format. It was designed to support different business processes and their invoicing requirements. The different processes are technically expressed in “profiles” (BASIC, BASIC-WL, EN16931, EXTENDED).

Building the PDF

There are a few different formats. I so far used ZUGFeRD / FacturX data format, or XRechnung specifically for Germany.

For PHP we can use an existing library to create those files, e.g.

  • easybill/zugferd-php

  • horstoeko/zugferd

and others. I personally used the latter so far.

I assume that a PDF building is already in place. If not, that’s a 30min thing with dompdf or mpdf etc.

You chose the profile that fits and

$documentBuilder = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_EN16931);

And set the information coming from an array or DTO:

$documentBuilder
    ->setDocumentInformation(
    	$this->invoiceDto->invoiceNumber,
    	ZugferdDocumentType::COMMERCIAL_INVOICE,
    	DateTime::createFromImmutable($this->invoiceDto->issueDate->toNative()),
    	$this->invoiceDto->currency,
    )
    ->setDocumentBusinessProcess('urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_1.2')
    ->setDocumentSupplyChainEvent(DateTime::createFromImmutable($this->invoiceDto->issueDate->toNative()))
    ->setDocumentSeller($this->invoiceDto->sellerAddress->name)
    ->addDocumentSellerTaxRegistration(ZugferdReferenceCodeQualifiers::VAT_REGI_NUMB, $this->invoiceDto->sellerAddress->vatNumber)
    ->setDocumentSellerAddress(
    	...
    )
    ->setDocumentSellerCommunication('EM', $this->invoiceDto->sellerAddress->email)
    ->setDocumentSellerContact(
    	...
    )
    ->setDocumentBuyer($this->invoiceDto->buyerAddress->name)
    ->setDocumentBuyerAddress(
    	...
    )
    ->setDocumentBuyerCommunication('EM', $this->invoiceDto->buyerAddress->email)
    ->addDocumentTax(
    	...
    ->setDocumentSummation(
    	...
    )
    ->addDocumentPaymentMean(
    	...
    );
    ->setDocumentBuyerReference($this->invoiceDto->reference);
    
$i = 0;
foreach ($this->invoiceDto->items as $item) {
    $documentBuilder
	->addNewPosition((string)++$i, null, 'INFORMATION')
	->...;
}

$xml = $documentBuilder->getContent();

Now we want to attach this XML to the invoice PDF:

$merger = new ZugferdDocumentPdfMerger($xml, file_get_contents($pathToPdf));
$result = $merger->generateDocument();
$string = $result->downloadString();
// Store PDF
file_put_contents($path, $string);

Validating the PDF

A basic validator is implemented in the library:

$errors = (new ZugferdDocumentValidator($document))->validateDocument();
if ($errors->count()) {
    // Report
}

I would recommend to use a service on top, that gives more detailed feedback, though:

If the PDF is valid, you can continue and mail your PDF to your customer or send it through the API connection to whatever automation of the business process is in place.

Generic or custom solution

When thinking about how to change the invoicing process, there can be certain pros and cons to generic solutions:

  • Generic eInvoicing solutions often require businesses to overhaul their existing sales systems, which can be costly and disruptive. Custom-built solutions can maybe more likely ensure seamless integration with current operations, eliminating unnecessary workflow adjustments.

  • If you have incorporated specialized workflows and business logic into your operations there will also be a higher chance to fit this without further complications.

Especially for smaller and medium businesses the custom solution can be a good and quick fit to adjust here an existing (PHP) backend.

Some considerations here:

  • Each invoice should have a unique identifier.

  • Since eInvoicing involves sensitive financial data, it is critical to use secure transmission protocols (e.g., HTTPS and API authentication mechanisms) to prevent data breaches and unauthorized access.

  • Businesses should maintain a proper audit trail for all eInvoices. This includes logging API requests, responses, and any validation errors to ensure traceability and compliance.

  • If an invoice submission is rejected due to validation errors or missing information, businesses should have an automated or manual process in place to correct and resubmit the invoice promptly.

Summary

Try to tackle this upgrade early on (summer/autumn) so that you and your business are fully prepared for next year.

Reach out if you have any questions or want consulting on an existing (backend) application of yours. Maybe I an help with the connection to the API or external systems you are using and getting the process up to date.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

One time login links in CakePHP 24 Apr 1:48 PM (7 months ago)

Sometimes also called magic links, these login links provide a way for a user to log in without the need of password entering.

Basic workflow

Instead of a “password reset” button to send you a mail to confirm your password change, the login page has a “send me a login link” button. You enter the same email, but the result now is a bit different. The mail contains a direct link that will log you in automatically.

This is – if the email sending is secure – a de-facto two-factor auth. And similarly safe than the reset password functionality, since that is the same email sending process.

Alternatively, you could also send a code (e.g. via phone messaging) to have to enter.

These one time logins should expire after usage, and also be quite limited in time (e.g. hours).

Passwordless login

If you take this one step further, you can even make the login completely passwordless.

  • User registers and gets the login link sent via email

  • User clicks the link and is directly logged in

And for the next visit

  • User enters email to send a login link again

  • Users logs in again

Implementation in CakePHP

We can build our link sending directly on top of Authentication plugin and the documented auth process. The login link functionality can be found in Tools plugin if you just want to quickly re-use it.

For this we add our identifier and authenticator after the already existing ones in Application.php:

service->loadIdentifier('Tools.LoginLink', [
    'resolver' => [
        'className' => 'Authentication.Orm',
    ],
]);
 
// Session, Form, Cookie first
$service->loadAuthenticator('Tools.LoginLink', [
    'urlChecker' => 'Authentication.CakeRouter',
    'url' => [
        'prefix' => false,
        'plugin' => false,
        'controller' => 'Account',
        'action' => 'login',
    ],    
]);

Now you should be able to log your users in on top of classic form login.

Mail sending

Send your user(s) an email with a login link. Four our new authenticator the expectation is your login action with ?token={token} appended.

You can generate one using:

$tokensTable = $this->fetchTable('Tools.Tokens');
$token = $tokensTable->newKey('login_link', null, $user->id);

If you want a custom token to be used:

$token = $tokensTable->newKey('login_link', '123', $user->id);

The mail sending is usually done via queue process. If you want to use the Queue plugin:

$queuedJobsTable = TableRegistry::getTableLocator()->get('Queue.QueuedJobs');
$data = [
    'to' => $user->email,
    'toName' => $user->full_name,
    'subject' => __('Login link'),
    'template' => 'login_link',
    'vars' => compact('token'),
];
$queuedJobsTable->createJob('Email', $data);

The template can be as simple as

A login-link has been requested for this email/user.

By clicking this link you will get logged in:
<?php echo $this->Url->build(['controller' => 'Account', 'action' => 'login', '?' => ['token' => $token]], ['fullBase' => true]);?>

Mark email as confirmed

When you not just send login link emails to already verified users, but if you replace the registration login process with such quick-logins, you might need to use a callback to set the email active when the authenticator gets called. This way your custom finder (e.g. 'active') will actually find the user then.

$service->loadIdentifier('Tools.LoginLink', [
    ...
    'preCallback' => function (int $id): void {
        // Sets email_confirmed
        TableRegistry::getTableLocator()->get('Users')->confirmEmail($id);
    },
]);

So once the user has been activated through a valid email (and this link click) in the pre-callback, the identifier is now able to find and return the user.

Security note

When implementing the “send login link” functionality, similar to “password reset”, you do not want to expose if that email is a valid user email on your system. So make sure the response is the same in both cases:

You validate the input and if format is valid, you continue. Return always a successful “If this email is in our system, you will receive a link” kind of text.

You ideally send the emails via Queue, as normal sending could be “measurable slower” due to the API call, and as such might also give an attacker information about the validity of an active email.

Summary

The authentication plugin is powerful and flexible. Adding a custom authenticator like this “LoginLink” on top is quite quick and easy.

Personally, I do like my passwords. Once the browser saved them for this page and form, I can just click+click and I am back in. Additionally, I use a password manager to keep track of them.

In cases where there is no information or on a mobile device it can sometimes be handy to quickly send yourself this email with a simple click to log back in. And if this avoids a double-opt-in (e.g. with Google Authenticator app), then that’s also super fast for users.

Let me know what you think, if this is something you would want to add to your apps.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?