Search by

flyo / nitro-yii2

nadar

Flyo Nitro Yii2 Framework Module

Package info

github.com/flyocloud/nitro-yii2

Type:yii2-extension

pkg:composer/flyo/nitro-yii2

Statistics

Installs: 2 024

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

3.9 2026-09-17 06:52 UTC

README

PHPUnit

composer require flyo/nitro-yii2

add the module to your config

'modules' => [
    'flyo' => [
        'class' => \Flyo\Yii\Module::class,
        'token' => 'YOUR_TOKEN',
        'liveEdit' => !YII_ENV_PROD, // this is the default behavior, set to `true` to enable live edit in production as well
    ]
]

add the cms page resolve to your views in the folder /views/nitro.php, all the routes from flyo nitro will now be resolved into this view file:

<?php
use Flyo\Yii\Widgets\PageWidget;
/** @var \Flyo\Model\Page $page */
?>
<h1><?= $page->getTitle(); ?>
<?= PageWidget::widget(['page' => $page]); ?>

In order to render those blocks use the Flyo\Yii\Widgets\PageWidget which will lookup all blocks inside the folder /views/flyo/*, so for instance you have a HeroTeaser component defined in flyo the view file is stored in /views/flyo/HeroTeaser.php with example content:

/** @var \Flyo\Model\Block $block */
print_r($block->getContent());
print_r($block->getConfig());
print_r($block->getItems());
print_r($block->getSlots());

Layout

Generate a navigation in the layout file, use the NavWidget:

<?php $nav = ContainerWidget::begin(['identifier' => 'main']) ?>
    <ul>
        <?php foreach ($nav->getItems() as $item): ?>
            <li><?= Html::a($item->getLabel(), $item->getHref()); ?></li>
        <?php endforeach; ?>
    </ul>
<?php $nav::end(); ?>

Layout blocks with children

An example where a block contains child blocks, defined in the slot content:

<?php
use Flyo\Yii\Widgets\BlockWidget;
/** @var \Flyo\Model\Block $block */
$config = $block->getConfig();
?>
<div class="container">
    <?php foreach ($block->getSlots()['content']->getContent() as $childBlock): ?>
        <div class="w-full">
            <?= BlockWidget::widget([
                'block' => $childBlock,
            ]); ?>
        </div>
    <?php endforeach; ?>
</div>
<?php SectionWidget::end(); ?>

Extend existing Routes

Its possible to extend the routing system for existing pages. This can help when building dynamic sub pages which need to ensure that you are still on the same CMS page (not not entity detail), in order to do add the following url rule in the UrlManager section:

'<path:(the-requested-slug)>/<slug:[a-z\-]+>' => 'flyo/nitro/index',

In order to link to extended route, its not possible to use Url::toRoute, since this is a fixed rule in routes anyhow you have to use:

<a href="/the-requested-slug/<?= ...; ?>">Detail</a>

Live Edit

As long as the liveEdit module property is enabled, the module registers the Nitro JS Bridge together with its boot script (page refresh, scroll to block, editor handshake and the click handlers for all elements with a data-flyo-uid attribute) whenever a page is rendered by a web application. This happens independently of the Editable widget, therefore live edit also works in projects which mark their blocks by hand or do not mark them at all, and it is switched on and off in one single place.

Yii2 Widget: Editable

This widget makes Flyo blocks editable inside the Flyo preview iframe by rendering the data-flyo-uid marker, which is picked up by the bridge registered through Live Edit.

Usage

Wrap content

<?php
use Flyo\Yii\Widgets\Editable;
?>

<?php Editable::begin(['block' => $block]); ?>
    <h2><?= $block->getTitle(); ?></h2>
    <p><?= $block->getText(); ?></p>
<?php Editable::end(); ?>

Renders:

<div data-flyo-uid="block-uid-here">
  <h2>…</h2>
  <p>…</p>
</div>

Attribute only

If you already have a wrapper element, use the static helper:

<section <?= Editable::attr($block) ?>>
  <h1><?= $block->getTitle(); ?></h1>
</section>

Renders:

<section data-flyo-uid="block-uid-here">
  <h1>…</h1>
</section>

Notes

Highlighting/click-to-edit works only inside Flyo’s preview iframe.

Outside preview, the page behaves normally.

Entity Detail and Draft Links

An entity detail is rendered with the EntityAction, the finder callable resolves the entity from the api:

public function actions()
{
    return [
        'detail' => [
            'class' => \Flyo\Yii\Actions\EntityAction::class,
            'finder' => fn (\Flyo\Api\EntitiesApi $api) => $api->entityBySlug(Yii::$app->request->get('slug')),
        ],
    ];
}

Flyo can hand out a draft link for an entity which is still offline: an expiring snapshot addressed by a token which takes the place of the unique id or the slug, so it is resolved through the very same call. Two things to keep in mind: a draft token does not look like a normal slug or unique id, therefore an url rule which validates the parameter against a pattern has to let it through, and typeId does not apply to a draft token.

Whenever the api answers with a draft, the action turns every cache layer off for that request — server page cache, cdn cache and client cache — and the response is sent with Cache-Control: no-store (plus no-store for the cdn headers and without the Last-Modified / Etag validators). A draft is deliberately never stored anywhere, so there is no copy left which could outlive the draft or hide a change made in the meantime.

Because that decision can only be made after the entity has been resolved, controllers which serve an EntityAction have to use Flyo\Yii\Filters\NitroPageCache instead of yii\filters\PageCache. It is the same filter, but it throws the recorded output away when the cache has been disabled during the request:

public function behaviors()
{
    return [
        [
            'class' => \Flyo\Yii\Filters\NitroPageCache::class,
            'only' => ['detail'],
            'enabled' => YII_ENV_PROD && \Flyo\Yii\Module::getInstance()->serverPageCache,
            'duration' => \Flyo\Yii\Module::getInstance()->serverPageCacheDuration,
            'dependency' => new \Flyo\Yii\Cache\VersionCacheDependency(),
            'variations' => [Yii::$app->request->getQueryParam('slug')],
        ],
    ];
}

In the view the draft state is available on the entity, use it to render a hint for the editor:

<?php if ($entity->getIsDraft()): ?>
    <p>Draft preview, this entity is not online yet.
       <?php if ($expiresAt = $entity->getDraftExpiresAt()): ?>
           The link expires on <?= Yii::$app->formatter->asDatetime((int) $expiresAt); ?>.
       <?php endif; ?>
    </p>
<?php endif; ?>

Any action which must not be cached can use the same switch: \Flyo\Yii\Module::getInstance()->disableCache();.

CDN Caching

In production the module writes Vercel-CDN-Cache-Control and CDN-Cache-Control on every response, they are the instructions for the edge cache in front of the application:

Vercel-CDN-Cache-Control: max-age=1800, stale-while-revalidate=900
CDN-Cache-Control: max-age=1800, stale-while-revalidate=900

max-age comes from cdnCacheDuration and says how long the edge serves a copy without asking the origin at all. stale-while-revalidate comes from cdnCacheStaleWhileRevalidateDuration and covers what happens afterwards: instead of every visitor waiting for a fresh render the moment the entry expires, the edge answers from the stale copy and refreshes itself with a single background request. Without it a popular url sends a burst of concurrent requests to the origin at every expiry, which is exactly when the origin is least able to take it.

'modules' => [
    'flyo' => [
        'class' => \Flyo\Yii\Module::class,
        'token' => 'YOUR_TOKEN',
        'cdnCacheDuration' => 1800,
        'cdnCacheStaleWhileRevalidateDuration' => 900, // set to 0 to omit the directive
    ]
]

Set cdnCache to false to tell the edge to store nothing, the client cache (clientHttpCache) stays independent of it. A request which called disableCache() always wins over all of this and is sent with no-store.

Server Page Cache and the Version Check

With serverPageCache enabled the rendered page and the nitro config are kept in the yii cache for serverPageCacheDuration seconds. Flyo\Yii\Cache\VersionCacheDependency makes that safe: it asks the flyo version api whether the content has changed, so a long duration never delivers outdated content.

Both switches hang on the same flag, which is worth knowing before turning it off:

serverPageCache flyo api calls per request which reaches the origin
true 1, the version check
false 2, the config and the page, both uncached

Turning the server page cache off does not make the module quieter, it makes it louder. It only removes the version check, and in exchange every request resolves the config and the page against the api again.

versionCheckInterval

Yii evaluates a cache dependency on every read of a cache entry, so by default the version api is asked once per request which reaches the origin. The answer is tiny, but php keeps no connection pool between requests, so it costs a dns lookup plus a tcp and a tls handshake before the request itself.

versionCheckInterval reuses the last answer for that many seconds. It is 0 (off) by default:

'flyo' => [
    'class' => \Flyo\Yii\Module::class,
    'token' => 'YOUR_TOKEN',
    'versionCheckInterval' => 300, // seconds, 0 disables the throttling
],

The api can not be asked more than once per interval, so the calls per day are capped at 86400 / interval. As long as requests arrive further apart than the interval, nothing is saved:

interval at most ... version calls per day saves nothing below ... requests per day
30s 2,880 2,880
60s 1,440 1,440
120s 720 720
300s 288 288

Share of the calls which is removed:

requests per day 30s 60s 120s 300s
1,000 0% 0% 28% 71%
5,000 42% 71% 86% 94%
10,000 71% 86% 93% 97%
50,000 94% 97% 99% 99%
1,000,000 100% 100% 100% 100%

Choosing the interval

What the interval costs is precision: content published in flyo is picked up by the server side cache up to this many seconds later. Pick it against the cache layer in front of it, not in absolute terms. With a cdn the edge already holds a copy for cdnCacheDuration plus cdnCacheStaleWhileRevalidateDuration, so a few minutes at the origin disappear in that window:

interval worst case a visitor can see, with the default 1800s + 900s cdn settings
0 45min 00s
30 45min 30s
60 46min 00s
120 47min 00s
300 50min 00s
  • 300 (5min) when a cdn is in front, which is the normal setup. It removes more than 90% of the calls from roughly 3,000 requests per day upwards and costs 5 of the 45 minutes the edge is stale anyway.
  • 30 to 60 when cdnCache is off and the server page cache is the only layer, because then the interval is the whole staleness a visitor can see.
  • 0 when an editor has to see a publish immediately without a cdn purge, for example on a staging or review deployment.

The value is stored in the cache which evaluates the dependency, so a shared cache (redis, memcached) means one call per interval for the whole cluster, a per node cache (file cache, and every serverless instance has its own) means one call per interval per node. A DummyCache stores nothing and therefore behaves exactly like 0.

Documentation

Read More about Flyo Nitro in general