-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathExtension.php
More file actions
205 lines (180 loc) · 10.3 KB
/
Extension.php
File metadata and controls
205 lines (180 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
<?php
declare(strict_types=1);
namespace Acme\Starter;
use Acme\Starter\Command\GreetCommand;
use TotalCMS\Domain\Extension\Data\AdminNavItem;
use TotalCMS\Domain\Extension\Data\DashboardWidget;
use TotalCMS\Domain\Extension\ExtensionContext;
use TotalCMS\Domain\Extension\ExtensionInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* Starter extension demonstrating every extension point.
*
* Use this as a template for building your own extensions.
* Delete the parts you don't need.
*/
class Extension implements ExtensionInterface
{
public function register(ExtensionContext $context): void
{
// ── Twig Functions ──────────────────────────────────────────────
// Available in all templates: {{ starter_greet('World') }}
$context->addTwigFunction(
new TwigFunction('starter_greet', function (string $name) use ($context): string {
$greeting = $context->setting('greeting', 'Hello');
return "{$greeting}, {$name}!";
})
);
// ── Twig Filters ────────────────────────────────────────────────
// Available in templates: {{ text|reverse_words }}
$context->addTwigFilter(
new TwigFilter('reverse_words', function (string $text): string {
return implode(' ', array_reverse(explode(' ', $text)));
})
);
// ── Twig Globals ────────────────────────────────────────────────
// Expose a value (object, array, or scalar) as a global variable in
// every template. Usage in templates: {{ starter.version }}
//
// $context->addTwigGlobal('starter', [
// 'version' => '1.0.0',
// ]);
// ── CLI Commands ────────────────────────────────────────────────
// Run with: tcms acme:greet --name=World
$context->addCommand(new GreetCommand());
// ── Admin Navigation ────────────────────────────────────────────
// Adds a link to the admin sidebar. Pass raw SVG — it's URL-encoded
// automatically by the template. Leave icon empty for the default
// puzzle piece icon.
$context->addAdminNavItem(new AdminNavItem(
label: 'Starter',
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="black" stroke-linecap="round" stroke-linejoin="round"><path d="M16 26L17.83 24.17C17.89 24.11 17.85 24 17.76 24H14.24C14.15 24 14.11 24.11 14.17 24.17L16 26Z" stroke="black" stroke-width="2" fill="none"/><path d="M16 26V30.5" stroke="black" stroke-width="2" fill="none"/><path d="M10.67 8L4 2.67V12.67L2 16L4.67 17.33L4 20.67L6.67 21.33L10.9 27.91C12 29.63 13.9 30.67 15.94 30.67H16.06C18.1 30.67 20 29.63 21.1 27.91L25.33 21.33L28 20.67L27.33 17.33L30 16L28 12.67V2.67L21.33 8C21.33 8 18.92 6.67 16 6.67C13.08 6.67 10.67 8 10.67 8Z" stroke="black" stroke-width="2" fill="none"/><circle cx="12" cy="17.5" r="2" fill="black"/><circle cx="20" cy="17.5" r="2" fill="black"/></g></svg>',
url: '/admin/ext/acme/starter/dashboard',
permission: 'admin',
priority: 80,
));
// ── Dashboard Widget ────────────────────────────────────────────
// Adds a widget to the admin home screen.
// Use @vendor-name/path.twig to reference templates in your templates/ dir.
$context->addDashboardWidget(new DashboardWidget(
id: 'starter-widget',
label: 'Starter Widget',
template: '@acme-starter/widgets/starter.twig',
position: 'sidebar', // 'main' or 'sidebar'
priority: 50,
));
// ── Event Listeners ─────────────────────────────────────────────
// React to content changes. Use $context->logger() to write to the
// shared extensions.log file (tcms-data/logs/extensions.log) on the
// 'extensions' channel. Prefix messages with your extension id so
// multi-extension logs stay readable.
$logger = $context->logger();
$context->addEventListener('object.created', function (array $payload) use ($logger): void {
// Called after any object is created in any collection.
// $payload contains: 'collection' and 'id'
$logger->info('[acme/starter] object.created', $payload);
});
$context->addEventListener('object.updated', function (array $payload) use ($logger): void {
$logger->info('[acme/starter] object.updated', $payload);
});
$context->addEventListener('object.deleted', function (array $payload) use ($logger): void {
// PSR-3 levels: debug, info, notice, warning, error, critical, alert, emergency.
// Pass a context array as the second argument for structured fields.
$logger->warning('[acme/starter] object.deleted', $payload);
});
// ── Custom Field Types ──────────────────────────────────────────
// Register a new field type usable in schemas (class must extend FormField)
$context->addFieldType('colorpicker', \Acme\Starter\Field\ColorPickerField::class);
// ── Assets (CSS / JS) ───────────────────────────────────────────
// CSS and JS files from assets/ are served at /ext/acme/starter/assets/
// with mtime-based cache busting. Admin assets render via
// {{ cms.adminAssetsHead/Body() }} (already wired in core admin
// templates); frontend assets render via {{ cms.assetsHead/Body() }}
// in your public theme template.
$context->addAdminAsset('css', 'colorpicker.css');
// $context->addAdminAsset('js', 'admin.js');
// $context->addFrontendAsset('css', 'widget.css');
// $context->addFrontendAsset('js', 'widget.js');
// Both methods accept the same optional arguments:
// position: 'head' | 'body' | null (null = CSS→head, JS→body)
// module: bool (JS only — <script type="module">, default true)
// preload: bool (emit a preload/modulepreload hint in head)
// version: ?string (override mtime cache-bust query string)
//
// $context->addFrontendAsset(
// type: 'js',
// path: 'widget.js',
// position: 'body',
// module: true,
// preload: true,
// );
// ── API Routes ──────────────────────────────────────────────────
// Protected API at /ext/acme/starter/... (requires session or API key)
$context->addRoutes(function ($group): void {
$group->get('/api/hello', Action\ApiHelloAction::class);
});
// ── Public Routes ───────────────────────────────────────────────
// Unauthenticated routes at /ext/acme/starter/... (no auth)
// Use for webhooks, embeds, and endpoints accessible without credentials.
$context->addPublicRoutes(function ($group): void {
$group->get('/status', Action\PublicStatusAction::class);
});
// ── Admin Routes ────────────────────────────────────────────────
// Admin pages at /admin/ext/acme/starter/... (requires login)
// Templates can extend admin-dashboard.twig for the admin layout.
$context->addAdminRoutes(function ($group): void {
$group->get('/dashboard', Action\DashboardAction::class);
});
// ── Container Definitions ───────────────────────────────────────
// Register a service in the DI container so other code can pull it
// via constructor injection or $context->get() during boot.
// The factory receives the Psr\Container\ContainerInterface.
//
// $context->addContainerDefinition(
// \Acme\Starter\Service\GeoIPService::class,
// fn () => new \Acme\Starter\Service\GeoIPService(),
// );
// ── Page Middleware ─────────────────────────────────────────────
// Register a middleware that builder pages can opt into via their
// `middleware` field — useful for auth gates, rate limits, geo
// redirects, A/B splits. The class must implement
// PageMiddlewareInterface (handle() returns ?ResponseInterface —
// null to continue, a Response to short-circuit) and be resolvable
// from the container — pair with addContainerDefinition().
//
// Names are stable contract: kebab-case, never rename once shipped
// or sites with the name in their page records will break.
//
// $context->addContainerDefinition(
// \Acme\Starter\Middleware\GeoRedirect::class,
// fn ($c) => new \Acme\Starter\Middleware\GeoRedirect(
// $c->get(\Acme\Starter\Service\GeoIPService::class),
// ),
// );
// $context->addPageMiddleware('geo-redirect', \Acme\Starter\Middleware\GeoRedirect::class);
}
public function boot(ExtensionContext $context): void
{
// The boot phase runs after ALL extensions have registered.
// Use $context->get() to resolve services from the DI container.
//
// Example: resolve a core service
// $config = $context->get(\TotalCMS\Support\Config::class);
// ── Installable Schemas ─────────────────────────────────────────
// Install a user-editable schema into tcms-data/.schemas/ (Pro+ only).
// Skips if the schema already exists. For read-only schemas managed
// by the extension, place them in the schemas/ directory instead.
//
// $context->installSchema([
// 'id' => 'starter-reviews',
// 'description' => 'Customer reviews',
// 'properties' => [
// 'rating' => ['type' => 'number', 'field' => 'number', 'label' => 'Rating'],
// 'review' => ['type' => 'string', 'field' => 'styledtext', 'label' => 'Review'],
// ],
// 'required' => ['id', 'rating'],
// 'index' => ['id', 'rating'],
// ]);
}
}