Changelog
phil/content-bundlePas encore traduite
Cette page n’est pas encore traduite en français ; elle est montrée en anglais.
All notable changes to phil/content-bundle will be documented in this file.
Updates should follow the Keep a CHANGELOG principles.
0.1.0-alpha - 2026-11-12
Added
- Dimensions: a content root and its rows by locale, stage and version, the shared row, fallback chains and
aggregation; the locale, the version and the chains are those of
phil/locale-dimensions, andDimensionLocalesraises their refusals asInvalidDimension. - Capabilities: template, workflow, SEO, excerpt, link, slug and purified body, each with its data mapper (none for
the workflow), merger, normalizer and mapping. A link to another site and a canonical URL are
Phil\Url\Url, fromphil/url, normalised; a slug isPhil\Slug\Slug, fromphil/slug; a submitted value that is neither is anInvalidContent. - The body's markup is
phil/markup's: the name of a format, a value ofMarkupFormat—markdown,html,bbcode,mediawiki,dokuwiki,creole,textandujo— or a format an add-on brings, the portsMarkupCompilerandHtmlPurifier(purifying atLevel::All), and their adaptersDefaultCompilerandSanitizerPurifier. A draft keeps the format its author writes in; a format the compiler does not know, and a UJO source that is not a valid document, are refused withInvalidContent. - PHP interface for migrations (
ContentManager), deletion of a root. - Versions: freezing on publication, fork to the next version, latest version.
- Publication draft → live on
symfony/workflow, the workflowphil_content_publication:Publication, a content in one locale;PublicationWorkflow;PublicationMarkingStore, which reads the draft row's place and publishes or takes offline throughContentManager; the servicephil_content.publication_workflow, added toworkflow.registrybyPublicationWorkflowPasswhen the container has it. The publication date comes from the requiredClockInterface, the container'sclockservice. Tested over in-memory ports with several locales, and in a kernel on SQLite, MySQL 8 and PostgreSQL. - The body's lifecycle: publishing stores on the live row the draft's body converted into UJO — the UJO as its source,
the format
ujo, the HTML compiled from it and purified —, through the portApplication\Publication\BodyConverterand its adapterUjoBodyConverter(phil/markup's readers atLevel::Forum,phil/ujo's encoder). What the conversion does not keep, through the portApplication\Contract\ConversionLossesand itsConversionLossvalues, whichUjoBodyConverterimplements for a format ofphil/markupor the name of a format an add-on brings, refuses the publication withApplication\Exception\PublicationLosesContent, which lists the losses, until the editor accepts them:ContentManager::publish(..., acceptLosses: true),phil:content:publish --accept-lossesor a yes to its question in a terminal, the contextaccept_lossesof thephil_content_publicationworkflow, or the box of the administration. The UJO'stimeis the moment of the publication, in milliseconds, from the clock. Raw HTML is published as text, arawblock of UJO included, unlessraw_html_rolenames a trusted role (off by default), checked by Symfony's authorization checker (RawHtmlRole, with the optionalsymfony/security-core, behind the portApplication\Contract\RawHtmlPermission) on the author of each revision, never on the publisher: the body records whether the editor who wrote its source held the role (Body::$rawHtml, the columnbody_raw_html,bodyRawHtmlin the normalized form), a save that leaves the source and the format unchanged keeps that author, and a working copy keeps the role of the editor who saved it, whoever applies it (Editing\AuthorRawHtml, wired byInfrastructure\Markup\RawHtmlAuthorPass, the columnraw_htmlofphil_content_working_copy). The raw HTML of an author who held the role is published asrawblocks, kept asphil/ujo'srawprofile allows, by any editor, and the draft, which its preview shows, renders it the same way (BodyDataMappercompiles it forLevel::All); the raw HTML of any other author is published as text, even by a publisher who holds the role. The conversion runs before the publication changes anything; the columns are unchanged. Removing a draft puts back the frozen draft rows of the published version, localized and shared, so the author's text returns in its own format; a frozen row that no longer exists is replaced by its live twin. - The events of
phil/publishing:ContentManagerdispatchesPagePublishedafter a publication, andPageRetiredafter a locale is taken offline or for each live locale of a deleted root, throughApplication\Publication\PageEvents— the SEO canonical URL or else the absolute URL of the current route, the BCP 47 tag of the locale, « indexable » unlessnoindex, the content reference as owner identifier —, read from the live row and sent once the transaction returned; the portApplication\Contract\CanonicalAddressesand its adapterContentCanonicalAddressesover the router,localhostaccepted in the kernel's debug mode only. - Scheduled publication: the port
Application\Contract\PublicationSchedule(schedule,cancel,pending) andScheduledPublication; withphil/scheduled-transitions-bundle, its adapterScheduledTransitionsPublicationScheduleover that bundle'sScheduler,PublicationReferences(news:42@fr) andPublicationSubjects, the resolver its workflow handler finds the publications by. - The dated archive of the kinds under
phil_content.archives— a « now » page —:ContentArchive(history(),read()) andArchivedVersion, read from the frozen draft rows;ArchiveController, which serves one version with the content's template; the Twig functioncontent_archive. - Single kinds,
phil_content.singlesandSingleKinds: a second root refused,ContentManager::single()and the Twig functioncontent_single. - Working copies:
Domain\Draft\WorkingCopy(tablephil_content_working_copy, DBAL typephil_content_working_copy_id), the private autosave of each author and the named drafts of a content, 256 KiB of JSON at most; the portDomain\Contract\WorkingCopyRepositoryand its adapterDoctrineWorkingCopyRepository;Application\Editing\WorkingCopies, which applies them throughContentManager::edit();ContentManager::delete()removes them. - The administration (
phil_content.admin):ContentAdminController— the list of contents by kind, creation, the editing page that saves, publishes, takes offline, discards, schedules, keeps and applies named drafts and restores the author's autosave, every form with its CSRF token —, its routes inconfig/routes_admin.php, its templates and its labels in thephil_contentdomain in English and French;ContentAdmin::menuItems()for an EasyAdmin dashboard; the Stimulus controllerphil--content-bundle--autosave. - The search feed of
phil/search-bundle:ContentDocumentProvider, one document per live localized row — title, the UJO passage of the row's language asbodyand the others asbody_<language>, the filterskind,localeandreference—,noindexleft out; the keyphil_content.search_index,contentby default (PhilContentBundle::SEARCH_INDEX), read without loading the provider when the search bundle is not installed. ContentManager::servedLocale()andexists().PublishedContentsandPublishedContent: what a locale publishes, the latest first, read a page at a time in a few queries whatever their number (find(PublishedRowQuery),chunks(),latest(..., $withSlug)), and the draft of a content.- The port
PublishedRowRepository, withPublishedRowQuery(filters on the root's fields and identifiers, rows with a slug, offset and limit),PublishedOrder(Root,LatestPublished,Slug) andPublishedRow; its Doctrine adapterDoctrinePublishedRowRepository, two queries per page;RootIdentifiers, shared withDoctrineContentRootRepository;ContentAggregator::mergeRows(), which merges rows already loaded;DimensionContentRepository::findVersions(), the rows of one locale in a stage, latest first. - Doctrine storage: base classes, mapping listener, DBAL types, repositories and transaction, tested on SQLite, MySQL 8 and PostgreSQL.
- Route table, slug history and 301 redirects, resolver, tested on SQLite, MySQL 8 and PostgreSQL;
RouteRepository::findCurrentAmong(), the current routes of many contents of one kind in one lookup. - Controller and Twig rendering (template by key, content, SEO tags, URLs), tested in a kernel;
ContentUrlGenerator::generateAll(), the URLs of many contents in one lookup of routes. ContentSchemaand the Twig functioncontent_schema: a normalized content as a schema.orgWebPage,Articleor the type a template asks for.- The sitemap through
phil/sitemap-bundle:ContentUrlProvider, aPhil\Sitemap\UrlProvidertaggedphil_sitemap.url_providerafter the sitemap'sRouteUrlProvider, gives the published contents, one section per kind, each locale walked in chunks; a kind whose key ends in an underscore and digits, asnews_2026, is the sectionnews-2026. FeedController(the Atom feed of a kind, itsfeed_limitentries with a slug read in one page),PreviewLinks,PreviewControllerandphil:content:preview-link(signed links to a draft); their routes inconfig/routes.php; the keyspreviewandfeed_limit;ResourceRoutes::resourceKeys().- Console commands: create, edit, publish, unpublish, list, tested in a kernel.
- The documentation in
doc/, one file per topic, indexed by the README. CONTRIBUTING.md,SECURITY.mdandCODE_OF_CONDUCT.md.