feat(demo): add story 1 — Sorano: Rock and Time
This commit is contained in:
@@ -0,0 +1,643 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Grav\Plugin\FlexObjects\Api;
|
||||
|
||||
use Grav\Common\Page\Media;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Plugin\Api\Controllers\AbstractApiController;
|
||||
use Grav\Plugin\Api\Controllers\HandlesMediaUploads;
|
||||
use Grav\Plugin\Api\Exceptions\NotFoundException;
|
||||
use Grav\Plugin\Api\Exceptions\ValidationException;
|
||||
use Grav\Plugin\Api\Response\ApiResponse;
|
||||
use Grav\Plugin\FlexObjects\Flex;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class FlexApiController extends AbstractApiController
|
||||
{
|
||||
use HandlesMediaUploads;
|
||||
|
||||
/**
|
||||
* GET /flex-objects/config
|
||||
*
|
||||
* Returns UI-relevant plugin configuration for admin-next. Never returns
|
||||
* secrets or backend-only data. The flex directories list is exposed
|
||||
* separately via GET /flex-objects so callers that just need config stay
|
||||
* lightweight.
|
||||
*/
|
||||
public function config(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requirePermission($request, 'api.access');
|
||||
|
||||
$cfg = $this->config->get('plugins.flex-objects', []);
|
||||
|
||||
return ApiResponse::create([
|
||||
'enabled' => (bool) ($cfg['enabled'] ?? true),
|
||||
'built_in_css' => (bool) ($cfg['built_in_css'] ?? true),
|
||||
'security' => [
|
||||
'restrict_page_frontmatter' => (bool) ($cfg['security']['restrict_page_frontmatter'] ?? true),
|
||||
],
|
||||
'admin_list' => [
|
||||
'per_page' => (int) ($cfg['admin_list']['per_page'] ?? 15),
|
||||
'order' => [
|
||||
'by' => (string) ($cfg['admin_list']['order']['by'] ?? 'updated_timestamp'),
|
||||
'dir' => (string) ($cfg['admin_list']['order']['dir'] ?? 'desc'),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /flex-objects — List all enabled flex directories with their admin config.
|
||||
*/
|
||||
public function directories(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requirePermission($request, 'api.access');
|
||||
|
||||
$flex = $this->getFlex();
|
||||
$user = $this->getUser($request);
|
||||
$result = [];
|
||||
|
||||
// Skip built-in types that already have dedicated admin-next UI
|
||||
$builtIn = ['pages', 'user-accounts', 'user-groups'];
|
||||
|
||||
foreach ($flex->getDirectories() as $directory) {
|
||||
if (!$directory->isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($directory->getFlexType(), $builtIn, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$config = $directory->getConfig('admin');
|
||||
if (empty($config) || !empty($config['disabled'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip directories the user cannot list
|
||||
if (!$this->isSuperAdmin($user) && !$directory->isAuthorized('list', 'admin', $user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$menu = $config['menu']['list'] ?? [];
|
||||
|
||||
// Resolve form field types for list columns so frontend can render properly
|
||||
$listFields = $config['list']['fields'] ?? [];
|
||||
$fieldTypes = [];
|
||||
try {
|
||||
$blueprint = $directory->getBlueprint();
|
||||
$formFields = $blueprint->fields();
|
||||
foreach (array_keys($listFields) as $fieldName) {
|
||||
$fieldTypes[$fieldName] = $formFields[$fieldName]['type'] ?? 'text';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Non-critical
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'type' => $directory->getFlexType(),
|
||||
'title' => $menu['title'] ?? $directory->getTitle(),
|
||||
'description' => $directory->getDescription() ?? '',
|
||||
'icon' => $menu['icon'] ?? 'fa-file',
|
||||
'list' => $config['list'] ?? [],
|
||||
'edit' => $config['edit'] ?? [],
|
||||
'search' => $directory->getConfig('data.search') ?? [],
|
||||
'field_types' => $fieldTypes,
|
||||
'export' => $config['export'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return ApiResponse::create($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /flex-objects/blueprints — List every available flex directory blueprint.
|
||||
*
|
||||
* Powers the `directories` field on the flex-objects plugin settings page:
|
||||
* one toggle per available blueprint, value is the array of enabled
|
||||
* blueprint URLs. Includes hidden + currently-disabled blueprints because
|
||||
* they're the things the admin is choosing to enable. The legacy URL
|
||||
* (pre-rc.4 alias) is included so the field can match saved values that
|
||||
* still reference the old form.
|
||||
*/
|
||||
public function blueprints(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requirePermission($request, 'api.access');
|
||||
|
||||
$flex = $this->getFlex();
|
||||
$newToOld = Flex::getLegacyBlueprintMap(false); // [newUrl => oldUrl]
|
||||
|
||||
$items = [];
|
||||
foreach ($flex->getBlueprints() as $directory) {
|
||||
$url = $directory->getBlueprintFile();
|
||||
$items[] = [
|
||||
'url' => $url,
|
||||
'legacy_url' => $newToOld[$url] ?? null,
|
||||
'type' => $directory->getFlexType(),
|
||||
'title' => $directory->getTitle(),
|
||||
'description' => $directory->getDescription(),
|
||||
];
|
||||
}
|
||||
|
||||
return ApiResponse::create($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /flex-objects/{type} — List objects with pagination, search, sort.
|
||||
*/
|
||||
public function index(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'list');
|
||||
|
||||
$pagination = $this->getPagination($request);
|
||||
$query = $request->getQueryParams();
|
||||
$search = $query['search'] ?? null;
|
||||
$sortField = $query['sort'] ?? null;
|
||||
$sortOrder = strtolower($query['order'] ?? 'asc');
|
||||
if (!in_array($sortOrder, ['asc', 'desc'], true)) {
|
||||
$sortOrder = 'asc';
|
||||
}
|
||||
|
||||
$collection = $directory->getCollection();
|
||||
|
||||
// Apply search
|
||||
if ($search && $search !== '') {
|
||||
$collection = $collection->search($search);
|
||||
}
|
||||
|
||||
// Apply sort
|
||||
if ($sortField) {
|
||||
$collection = $collection->sort([$sortField => $sortOrder]);
|
||||
}
|
||||
|
||||
$total = $collection->count();
|
||||
|
||||
// Slice for pagination
|
||||
$objects = $collection->slice($pagination['offset'], $pagination['limit']);
|
||||
|
||||
// Get list field names from config
|
||||
$listFields = array_keys($directory->getConfig('admin.list.fields') ?? []);
|
||||
|
||||
$data = [];
|
||||
foreach ($objects as $object) {
|
||||
$data[] = $this->serializeForList($object, $listFields);
|
||||
}
|
||||
|
||||
return ApiResponse::paginated(
|
||||
data: $data,
|
||||
total: $total,
|
||||
page: $pagination['page'],
|
||||
perPage: $pagination['per_page'],
|
||||
baseUrl: $this->getApiBaseUrl() . '/flex-objects/' . $type,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /flex-objects/{type}/{key} — Get a single object.
|
||||
*/
|
||||
public function show(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'read');
|
||||
|
||||
$key = $this->getRouteParam($request, 'key');
|
||||
$object = $directory->getObject($key);
|
||||
|
||||
if (!$object) {
|
||||
throw new NotFoundException("Object '{$key}' not found in '{$type}'.");
|
||||
}
|
||||
|
||||
return $this->respondWithEtag($this->serializeObject($object));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /flex-objects/{type} — Create a new object.
|
||||
*/
|
||||
public function create(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'create');
|
||||
|
||||
$body = $this->getRequestBody($request);
|
||||
unset($body['__meta']);
|
||||
|
||||
try {
|
||||
$object = $directory->createObject($body, '');
|
||||
$object->save();
|
||||
} catch (\Exception $e) {
|
||||
throw new \Grav\Plugin\Api\Exceptions\ValidationException(
|
||||
'Failed to create object: ' . $e->getMessage(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->fireAdminEvent('onAdminAfterSave', ['object' => $object]);
|
||||
|
||||
$key = $object->getKey();
|
||||
|
||||
return ApiResponse::created(
|
||||
data: $this->serializeObject($object),
|
||||
location: $this->getApiBaseUrl() . '/flex-objects/' . $type . '/' . $key,
|
||||
headers: $this->invalidationHeaders([
|
||||
'flex-objects:' . $type . ':list',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /flex-objects/{type}/{key} — Update an existing object.
|
||||
*/
|
||||
public function update(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'update');
|
||||
|
||||
$key = $this->getRouteParam($request, 'key');
|
||||
$object = $directory->getObject($key);
|
||||
|
||||
if (!$object) {
|
||||
throw new NotFoundException("Object '{$key}' not found in '{$type}'.");
|
||||
}
|
||||
|
||||
// ETag validation
|
||||
$currentEtag = $this->generateEtag($this->serializeObject($object));
|
||||
$this->validateEtag($request, $currentEtag);
|
||||
|
||||
$body = $this->getRequestBody($request);
|
||||
unset($body['__meta']);
|
||||
|
||||
try {
|
||||
$object->update($body);
|
||||
$object->save();
|
||||
} catch (\Exception $e) {
|
||||
throw new \Grav\Plugin\Api\Exceptions\ValidationException(
|
||||
'Failed to update object: ' . $e->getMessage(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->fireAdminEvent('onAdminAfterSave', ['object' => $object]);
|
||||
|
||||
return $this->respondWithEtag(
|
||||
$this->serializeObject($object),
|
||||
200,
|
||||
['flex-objects:' . $type . ':list', 'flex-objects:' . $type . ':update:' . $key],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /flex-objects/{type}/{key} — Delete an object.
|
||||
*/
|
||||
public function delete(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'delete');
|
||||
|
||||
$key = $this->getRouteParam($request, 'key');
|
||||
$object = $directory->getObject($key);
|
||||
|
||||
if (!$object) {
|
||||
throw new NotFoundException("Object '{$key}' not found in '{$type}'.");
|
||||
}
|
||||
|
||||
$object->delete();
|
||||
|
||||
$this->fireAdminEvent('onAdminAfterDelete', ['object' => $object]);
|
||||
|
||||
return ApiResponse::noContent(
|
||||
$this->invalidationHeaders([
|
||||
'flex-objects:' . $type . ':list',
|
||||
'flex-objects:' . $type . ':delete:' . $key,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /flex-objects/{type}/export — Export all objects as YAML.
|
||||
*/
|
||||
public function export(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'list');
|
||||
|
||||
$collection = $directory->getCollection();
|
||||
$data = [];
|
||||
|
||||
foreach ($collection as $object) {
|
||||
$data[$object->getKey()] = $object->jsonSerialize();
|
||||
}
|
||||
|
||||
$yaml = \Grav\Common\Yaml::dump($data, 10, 2);
|
||||
$filename = $type . '-' . date('Y-m-d') . '.yaml';
|
||||
|
||||
return new \Grav\Framework\Psr7\Response(
|
||||
200,
|
||||
[
|
||||
'Content-Type' => 'application/x-yaml',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
'Cache-Control' => 'no-store, max-age=0',
|
||||
],
|
||||
$yaml,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /flex-objects/{type}/{key}/media — List media attached to an object.
|
||||
*
|
||||
* For folder-stored directories the media lives in the object's own
|
||||
* storage folder (e.g. user-data://flex-objects/contacts/{id}), alongside
|
||||
* the object's data file.
|
||||
*/
|
||||
public function mediaList(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'read');
|
||||
|
||||
$object = $this->resolveObject($directory, $request);
|
||||
$folder = $this->resolveMediaFolder($object);
|
||||
|
||||
$media = new Media($folder);
|
||||
$serialized = $this->getSerializer()->serializeCollection($media->all());
|
||||
|
||||
return ApiResponse::create($serialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /flex-objects/{type}/{key}/media — Upload file(s) to an object.
|
||||
*/
|
||||
public function mediaUpload(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'update');
|
||||
|
||||
$object = $this->resolveObject($directory, $request);
|
||||
$key = $object->getKey();
|
||||
$folder = $this->resolveMediaFolder($object);
|
||||
|
||||
if (!is_dir($folder) && !mkdir($folder, 0775, true) && !is_dir($folder)) {
|
||||
throw new ValidationException('Unable to create media directory for this object.');
|
||||
}
|
||||
|
||||
$uploadedFiles = $this->flattenUploadedFiles($request->getUploadedFiles());
|
||||
if ($uploadedFiles === []) {
|
||||
throw new ValidationException('No files were uploaded.');
|
||||
}
|
||||
|
||||
// Honor per-field upload settings (random_name, accept, ...) forwarded
|
||||
// by the file field; absent, this is an inert no-op.
|
||||
$settings = $this->parseUploadFieldSettings($request);
|
||||
|
||||
$uploadedNames = [];
|
||||
foreach ($uploadedFiles as $file) {
|
||||
// Fire before event — plugins can throw to reject specific files
|
||||
$this->fireEvent('onApiBeforeMediaUpload', [
|
||||
'object' => $object,
|
||||
'filename' => $file->getClientFilename(),
|
||||
'type' => $file->getClientMediaType(),
|
||||
'size' => $file->getSize(),
|
||||
]);
|
||||
|
||||
$uploadedNames[] = $this->processUploadedFile($file, $folder, $settings);
|
||||
}
|
||||
|
||||
// Fresh Media object to pick up the newly uploaded files
|
||||
$media = new Media($folder);
|
||||
$serialized = $this->getSerializer()->serializeCollection($media->all());
|
||||
|
||||
$this->fireAdminEvent('onAdminAfterAddMedia', ['object' => $object]);
|
||||
$this->fireEvent('onApiMediaUploaded', [
|
||||
'object' => $object,
|
||||
'filenames' => $uploadedNames,
|
||||
]);
|
||||
|
||||
return ApiResponse::created(
|
||||
data: $serialized,
|
||||
location: $this->getApiBaseUrl() . '/flex-objects/' . $type . '/' . $key . '/media',
|
||||
headers: $this->invalidationHeaders([
|
||||
'flex-objects:' . $type . ':media:' . $key,
|
||||
'flex-objects:' . $type . ':update:' . $key,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /flex-objects/{type}/{key}/media/{filename} — Delete a media file.
|
||||
*/
|
||||
public function mediaDelete(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
$directory = $this->resolveDirectory($type);
|
||||
$this->requireFlexPermission($request, $directory, 'update');
|
||||
|
||||
$object = $this->resolveObject($directory, $request);
|
||||
$key = $object->getKey();
|
||||
$folder = $this->resolveMediaFolder($object);
|
||||
$filename = $this->getSafeFilename($request);
|
||||
|
||||
$filePath = $folder . '/' . $filename;
|
||||
if (!file_exists($filePath)) {
|
||||
throw new NotFoundException("Media file '{$filename}' not found on this object.");
|
||||
}
|
||||
|
||||
$this->fireEvent('onApiBeforeMediaDelete', ['object' => $object, 'filename' => $filename]);
|
||||
|
||||
unlink($filePath);
|
||||
|
||||
// Also remove any metadata sidecar (.meta.yaml) if it exists
|
||||
$metaPath = $filePath . '.meta.yaml';
|
||||
if (file_exists($metaPath)) {
|
||||
unlink($metaPath);
|
||||
}
|
||||
|
||||
$this->fireAdminEvent('onAdminAfterDelMedia', ['object' => $object, 'filename' => $filename]);
|
||||
$this->fireEvent('onApiMediaDeleted', ['object' => $object, 'filename' => $filename]);
|
||||
|
||||
return ApiResponse::noContent(
|
||||
$this->invalidationHeaders([
|
||||
'flex-objects:' . $type . ':media:' . $key,
|
||||
'flex-objects:' . $type . ':update:' . $key,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve the {key} route param to an existing object or throw a 404.
|
||||
*/
|
||||
private function resolveObject(FlexDirectory $directory, ServerRequestInterface $request): FlexObjectInterface
|
||||
{
|
||||
$key = $this->getRouteParam($request, 'key');
|
||||
$object = $key !== null && $key !== '' ? $directory->getObject($key) : null;
|
||||
|
||||
if (!$object) {
|
||||
$type = $directory->getFlexType();
|
||||
throw new NotFoundException("Object '{$key}' not found in '{$type}'.");
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an object's media folder to an absolute, writable filesystem path.
|
||||
*
|
||||
* getMediaFolder() returns null for SimpleStorage directories (a single
|
||||
* shared file, no per-object folder) and a GRAV_ROOT-relative or stream
|
||||
* path for folder-stored ones. Normalize all cases to an absolute path.
|
||||
*/
|
||||
private function resolveMediaFolder(FlexObjectInterface $object): string
|
||||
{
|
||||
$folder = method_exists($object, 'getMediaFolder') ? $object->getMediaFolder() : null;
|
||||
if (!$folder) {
|
||||
throw new ValidationException(
|
||||
'This directory does not support per-object media. '
|
||||
. 'Object media requires folder-based storage.',
|
||||
);
|
||||
}
|
||||
|
||||
/** @var \RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator $locator */
|
||||
$locator = $this->grav['locator'];
|
||||
if ($locator->isStream($folder)) {
|
||||
// Resolve to the absolute writable path, even if it doesn't exist yet
|
||||
$resolved = $locator->findResource($folder, true, true);
|
||||
if ($resolved) {
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
|
||||
// Already absolute? Use as-is. Otherwise treat as GRAV_ROOT-relative.
|
||||
if (str_starts_with($folder, '/') || preg_match('#^[A-Za-z]:[\\\\/]#', $folder)) {
|
||||
return $folder;
|
||||
}
|
||||
|
||||
return rtrim(GRAV_ROOT, '/') . '/' . $folder;
|
||||
}
|
||||
|
||||
private function getFlex(): Flex
|
||||
{
|
||||
return $this->grav['flex_objects'];
|
||||
}
|
||||
|
||||
private function resolveDirectory(?string $type): FlexDirectory
|
||||
{
|
||||
if (!$type) {
|
||||
throw new NotFoundException('Flex directory type is required.');
|
||||
}
|
||||
|
||||
$flex = $this->getFlex();
|
||||
$directory = $flex->getDirectory($type);
|
||||
|
||||
if (!$directory || !$directory->isEnabled()) {
|
||||
throw new NotFoundException("Flex directory '{$type}' not found or not enabled.");
|
||||
}
|
||||
|
||||
return $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the directory-specific permission derived from the blueprint.
|
||||
*
|
||||
* Checks both api.* and admin.* prefixed permissions (OR logic) so users
|
||||
* with either grant can access the flex directory via the API.
|
||||
*/
|
||||
private function requireFlexPermission(
|
||||
ServerRequestInterface $request,
|
||||
FlexDirectory $directory,
|
||||
string $action,
|
||||
): void {
|
||||
$user = $this->getUser($request);
|
||||
|
||||
if ($this->isSuperAdmin($user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check API access
|
||||
if (!$this->hasPermission($user, 'api.access')) {
|
||||
throw new \Grav\Plugin\Api\Exceptions\ForbiddenException('API access is not enabled for this user.');
|
||||
}
|
||||
|
||||
// Check directory-level permission from blueprint config.
|
||||
// Blueprints may define both admin.* and api.* permissions — check all
|
||||
// registered prefixes (OR: any matching permission grants access).
|
||||
$permissions = $directory->getConfig('admin.permissions');
|
||||
if ($permissions) {
|
||||
foreach ($permissions as $prefix => $config) {
|
||||
$permission = $prefix . '.' . $action;
|
||||
if ($this->hasPermission($user, $permission)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// None matched — report the first prefix for a clear error
|
||||
$prefix = array_key_first($permissions);
|
||||
throw new \Grav\Plugin\Api\Exceptions\ForbiddenException("Missing required permission: {$prefix}.{$action}");
|
||||
}
|
||||
}
|
||||
|
||||
private function serializeObject(FlexObjectInterface $object): array
|
||||
{
|
||||
$data = $object->jsonSerialize();
|
||||
|
||||
return array_merge(
|
||||
['key' => $object->getKey(), '__meta' => $this->objectMeta($object)],
|
||||
is_array($data) ? $data : [],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only metadata for the admin "object info" panel: the identifier used
|
||||
* in code snippets plus where the object lives on disk. Returned under the
|
||||
* reserved `__meta` key so the admin can show it without it ever becoming
|
||||
* part of the saved object data.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function objectMeta(FlexObjectInterface $object): array
|
||||
{
|
||||
$meta = [
|
||||
'type' => $object->getFlexType(),
|
||||
'key' => $object->getKey(),
|
||||
'storageKey' => $object->getStorageKey(),
|
||||
];
|
||||
|
||||
// Storage folder comes from the media trait, not the object interface,
|
||||
// so guard it — some storages return null until the object is saved.
|
||||
if (method_exists($object, 'getStorageFolder')) {
|
||||
$folder = $object->getStorageFolder();
|
||||
if ($folder) {
|
||||
$meta['storagePath'] = $folder;
|
||||
}
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
private function serializeForList(FlexObjectInterface $object, array $listFields): array
|
||||
{
|
||||
$data = ['key' => $object->getKey()];
|
||||
|
||||
if ($listFields) {
|
||||
foreach ($listFields as $field) {
|
||||
$data[$field] = $object->getProperty($field);
|
||||
}
|
||||
} else {
|
||||
// No list config — return all data
|
||||
$all = $object->jsonSerialize();
|
||||
if (is_array($all)) {
|
||||
$data = array_merge($data, $all);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Grav\Plugin\FlexObjects\Api;
|
||||
|
||||
use Grav\Plugin\Api\Controllers\BlueprintController;
|
||||
use Grav\Plugin\Api\Exceptions\NotFoundException;
|
||||
use Grav\Plugin\Api\Response\ApiResponse;
|
||||
use Grav\Plugin\FlexObjects\Flex;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
class FlexBlueprintController extends BlueprintController
|
||||
{
|
||||
/**
|
||||
* GET /blueprints/flex-objects/{type} — Serve the flex directory blueprint for form rendering.
|
||||
*/
|
||||
public function flexBlueprint(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requirePermission($request, 'api.access');
|
||||
|
||||
$type = $this->getRouteParam($request, 'type');
|
||||
|
||||
/** @var Flex $flex */
|
||||
$flex = $this->grav['flex_objects'];
|
||||
$directory = $flex->getDirectory($type);
|
||||
|
||||
if (!$directory || !$directory->isEnabled()) {
|
||||
throw new NotFoundException("Flex directory '{$type}' not found or not enabled.");
|
||||
}
|
||||
|
||||
$blueprint = $directory->getBlueprint();
|
||||
$data = $this->serializeBlueprint($blueprint, $type);
|
||||
|
||||
// Fire event to allow plugins to modify the serialized blueprint fields
|
||||
$event = new Event([
|
||||
'fields' => $data['fields'],
|
||||
'template' => 'flex-objects/' . $type,
|
||||
'user' => $this->getUser($request),
|
||||
]);
|
||||
$this->grav->fireEvent('onApiBlueprintResolved', $event);
|
||||
$data['fields'] = $event['fields'];
|
||||
|
||||
return ApiResponse::create($data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user