Aller au contenu

omnibase/agenda

Concert dates and venues for omnibase: an Event is a Thread; the agenda page, ICS feeds and imports, Google Calendar links, JSON-LD MusicEvent.

Étage
Structure préparée · Omnibase
Version
main
Vérifié en réel
Son README ne le dit pas
Licence
MIT
composer require omnibase/agenda:dev-main

Dates for the sites built on omnisense/omnibase: a harpist's concerts, a violinist's, a school's open days, a shop's late openings - where they happen, with whom, what is played, where the tickets are; the calendar visitors subscribe to, the "add to my calendar" links, the same dates as JSON for other sites and apps, and the dates read from where they are kept: a Google Calendar the artist fills in from her phone, an agency's feed, a former Squarespace site, another omnibase site. It is a Symfony bundle (Base\Agenda\AgendaBundle) on the core 3 or 4, with its back office on omnibase/admin.

An Event is an omnibase Thread (a JOINED subclass): a title (the concert's name), a headline, an excerpt, the text, tags, owners, a slug, publish states and scheduling, revisions, soft delete, translations; the bundle adds the moment (startsAt, endsAt, the hall's timezone, allDay), the venue, the ensemble and the conductor, the artist's role, the programme and the performers, the tickets' and the organiser's links, a cover, and cancelled - a cancelled date stays on the page, struck through: who had a ticket must find it. A Venue is a plain row: typed once, chosen on every date.

The artist never needs to log into the site: agenda:sync reads her calendars, and the back office shows what a sync would change before it is applied.

How this manual is organised

Proposer une modification docs/index.md

Installation

Requirements

What composer.json requires: PHP 8.2 or later, doctrine/orm 2.15 or 3, omnisense/omnibase 3.0 or 4.0, omnibridge/symfony 1.x, omnibase/admin, twig/intl-extra 3 (the templates' date filters), and symfony/config, console, dependency-injection, form, http-client (reading the calendars), http-kernel, security-bundle and string 6, 7 or 8. It suggests omnibase/newsletter: the coming dates in its monthly digest.

The bundle

// config/bundles.php
return [
    // ...
    Base\BaseBundle::class => ['all' => true],
    Base\Admin\AdminBundle::class => ['all' => true],
    Base\Agenda\AgendaBundle::class => ['all' => true],
];

AgendaBundle aliases Base\Agenda\Entity\* to App\Entity\Agenda\* and the repositories to App\Repository\Agenda\*, unless the application declares its own; templates/ is @Agenda, public/ is bundles/agenda/.

The routes

# config/routes.yaml
agenda_controller:
    resource: "@AgendaBundle/src/Controller/Client"
    type: attribute
    prefix: /

# the back office's Google Calendar page (omnibase/admin)
agenda_admin_controller:
    resource: "@AgendaBundle/src/Controller/Admin/CalendarController.php"
    type: attribute
    prefix: /

and the page in the dashboard's menu:

MenuItem::linkToRoute('agenda_admin_calendar', [], 'Google Calendar', 'fa-brands fa-google'),

Configuration

# config/packages/agenda.yaml (every key optional)
agenda:
    sources: []
    ics: ~                   # '%env(AGENDA_ICS)%' for a secret address
    past_per_page: 24
    timezone: Europe/Berlin
    calendar_name: ~
    jsonld: true
    jsonld_type: MusicEvent
    show_past: true

The schema and the public files

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
bin/console assets:install

Tables agenda_event (joined to the core's thread table) and agenda_venue; the stylesheet bundles/agenda/css/agenda.css.

The sync

*/30 * * * *  bin/console agenda:sync

What the layout provides

The pages extend layout1.html.twig and fill title, description, content and stylesheets (where /agenda also announces its JSON feed with a <link rel="alternate" type="application/json">). They use twig/intl-extra's date filters and the core's |wysiwyg.

Proposer une modification docs/installation.md

Dates and venues

Event

Base\Agenda\Entity\Event extends the core's Thread (agenda_event, discriminator agenda_event, indexed on startsAt):

Property Column Meaning
startsAt datetime_immutable, required stored as given; read on the hall's clock with getStartsAtLocal()
endsAt nullable for whole days, the last day
timezone string(64) the hall's (agenda.timezone for a new date)
allDay boolean
venue many-to-one Venue, SET NULL
ensemble, conductor string(180)
role string(24) a Role value: soloist, leader (concertmaster), chamber, orchestra, recital, masterclass, other (default); Role::of() reads anything, other when unknown
programme text one work per line: "Glière — Harp Concerto op. 74"
performers text one per line: "Anna Meyer, harp"
ticketsUrl, eventUrl string(500), URLs where the tickets are; the organiser's page
cancelled boolean struck through, never removed
cover upload, image, 8 MB
sourceUid (unique), source, sourceUpdatedAt the calendar entry it was read from
syncLocked boolean "Keep the site's texts": a sync brings only its hours and its cancellation

Methods:

  • getStartsAtLocal(), getEndsAtLocal(), getDateTimeZone() (UTC when the timezone is not one);
  • isMultiDay(); isUpcoming(?$now) - its (last) day not over, on the hall's clock; isPast(); isToday(?$now) - not cancelled, today in its own timezone or within its run; isTonight() - today and starting at 17:00 or later, not all day; isOnStage() - started and not over (two hours without an end);
  • getProgrammeLines(), getPerformerLines(), getProgrammeWorks() ({composer, work} split on a dash between spaces or a colon), getPerformerNames() ({name, part});
  • getPictureUrl() - its cover, else its venue's photo; getPictureCredit() - the venue's photo credit when the picture is the venue's;
  • getDescriptionText() - what a calendar entry says under the title, in the words the sync reads back (Orchestra:, Conductor:, Role:, With:, Tickets:, Programme:);
  • toCalendarEntry(string $uid, ?string $url) - the core's CalendarEntry (null without a start), two hours (Event::DEFAULT_DURATION, PT2H) without an end;
  • duplicate(string $titleSuffix) - a copy to start the next date of a tour from: same times, venue, programme, people and links, its owners, no calendar behind it;
  • isImported() - read from a calendar.

Venue

agenda_venue: name (not blank), hall, address, postcode, city, country (ISO), mapUrl, website, latitude, longitude, photo (8 MB), photoCredit, aliases (the spellings merged into it), slug (unique, given at the first save from the name and town, -2 when taken).

getLabel() ("Elbphilharmonie, Hamburg"), getFullAddress() (name, hall, address, postcode and town, country), getMapLink() (the map typed, else an OpenStreetMap point from the coordinates, else a search of the address), isKnownAs(string $name, ?string $city) (its own name and town or an alias, case and accents aside), Venue::key(), getPhotoUrl().

Repositories

Repository Methods
EventRepository findUpcoming(?int $limit) (published dates whose day is not over, nearest first), countUpcoming(), findToday(?$now), findPast(?int $limit, int $page), countPast(), findOnePublished(string $slug), findBetween($from, $to), findSince($from), findForFeed(bool $past, ?$from, ?$to, int $limit, int $page) (one more than the limit, to know there is a next page), findAllPublished(), findByMonth(), findBySourceUid(), findUpcomingOfSource()
VenueRepository findOneByNameAndCity(), findOneByFullAddress(), findOneByAlias(), merge(Venue $from, Venue $into): int, findAllByName()

merge() moves every date of $from (drafts and trashed ones included) to $into, fills what $into lacks from $from (hall, address, postcode, country, map, site, coordinates, the photo copied with its credit), keeps $from's spellings as aliases of $into, deletes $from, and returns the dates moved; a house is never merged into itself.

Service\Months::group($events) groups dates by month for the pages (agenda_months()).

Proposer une modification docs/concepts.md

The pages and the JSON feed

Route Path Methods
agenda_index /agenda any the dates to come by month, the past ones folded; sitemap 0.8, weekly
agenda_event /agenda/{slug} any one date, its programme, its JSON-LD; sitemap 0.6, weekly
agenda_feed /agenda.ics any ICS feeds
agenda_event_ics /agenda/{slug}.ics any
agenda_json /agenda.json GET, HEAD the dates as JSON
agenda_event_json /agenda/{slug}.json GET, HEAD one date as JSON
agenda_event_cover /agenda/{slug}/cover GET, HEAD a date's cover with its type
agenda_venue_photo /agenda/venue/{slug}/photo GET, HEAD a house's photo with its type

A slug that names no published date is a 404. Every date's page is in /sitemap.xml (EventListener\SitemapListener, on the core's SitemapEvent::BUILD); the calendars and the feeds are not.

/agenda

upcoming and months (the dates to come, by month); with agenda.show_past, the past dates (past, past_months) folded under them, agenda.past_per_page a page - ?past=1&page=2 opens them.

/agenda.json

Anyone may read it, from anywhere: Content-Type: application/json, Access-Control-Allow-Origin: *, Cache-Control: public, max-age=300, an ETag of its content (the moment it was written aside) - a reader sending it back in If-None-Match gets a 304.

Query
(none) the dates to come, the nearest first
?past=1 the dates gone by, the latest first
?from=2026-11-01&to=2026-12-31 a span of days, both included; a malformed day is a 400 with {"error": ...}
?limit= 100 by default, 1 to 500
?page= the page; next is the next one's address, null on the last

The document (Service\Feed::document()) is {calendar, events, next}: calendar holds the name, url, timezone, ics and generatedAt; each event (Feed::event()) id, slug, url, ics, title, start, end, timezone, allDay, cancelled, role, roleLabel (in the request's language), ensemble, conductor, performers and programme (lists), tickets, eventUrl, image and imageCredit (the picture the date shows: its cover, else its house's photo and the photographer), venue (name, hall, address, postcode, city, country, latitude, longitude, map, photo), updatedAt.

id is the calendar entry's UID when the date was read from one, else <id>@<host> - the same as in the ICS files. start and end carry the hall's offset; a date of whole days starts at midnight, allDay true, end its last day. Service\FeedParser reads the document back: one omnibase site can take another's dates (Where the dates come from).

/agenda/{slug}.json is Feed::event() alone, with the same headers.

The pictures

/agenda/{slug}/cover and /agenda/venue/{slug}/photo serve the upload with its own type, CORS open, cached a day (public), 304 when not modified; a 404 without an image. They are the addresses the JSON feed and the JSON-LD give; the pages' own <img> keep the files' addresses.

Proposer une modification docs/usage-pages.md

ICS feeds and Google Calendar links

The iCalendar writing (RFC 5545) and the Google Calendar link are omnisense/omnibase's Base\Service\Calendar\{Ics, GoogleCalendarLink}, on a CalendarEntry. The agenda maps its dates there:

  • Event::toCalendarEntry(string $uid, ?string $url): the date as a calendar sees it (the hall's timezone, whole days, the venue's address and GPS point, cancelled or not, Event::DEFAULT_DURATION without an end).
  • Base\Agenda\Service\Ics: calendar($events, $name) and event($event) (the /agenda.ics and /agenda/{slug}.ics feeds), google($event) (the Twig function agenda_google_url()), uid($event) (the source's UID, else <id>@<host>), entry($event). Ics::escape() and Ics::fold() are the core's.

Nothing to configure.

What the files hold

The files are written by the core's Base\Service\Calendar\Ics (RFC 5545): CRLF, lines folded at 75 octets, texts escaped, the hour in the hall's timezone with its VTIMEZONE, whole days as dates, STATUS:CANCELLED, and the UID of the entry a date was imported from - so a calendar that holds both never shows it twice. PRODID is -//omnibase//agenda//EN.

Address Holds Headers
/agenda.ics (agenda_feed) every published date from a year back, named agenda.calendar_name (else the site's title, else "Agenda") text/calendar; charset=utf-8, inline agenda.ics, Cache-Control: public, max-age=900
/agenda/{slug}.ics (agenda_event_ics) one date, named after its title the same, <slug>.ics

The page offers webcal://…/agenda.ics (agenda_webcal_url()): a phone, Apple Calendar or Outlook subscribes to it and follows the changes; Google Calendar takes the same address in Other calendars → From URL. Each date has two links more: Google Calendar's template page (agenda_google_url(event)) and its .ics (agenda_ics_url(event)); @Agenda/client/_calendar_links.html.twig renders both.

Proposer une modification docs/calendar.md

Where the dates come from

The artist (or her agency) keeps the dates where she already keeps them. agenda:sync reads every source and applies what changed; the back office reads them on demand and shows what they would change before anything is applied.

bin/console agenda:sync                          # every source
bin/console agenda:sync --dry-run                # what would change, each field as it was and as it becomes; nothing applied
bin/console agenda:sync --source=<address|file>  # one instead (repeatable; not remembered as the last sync)
bin/console agenda:sync -v                       # the changes too

With no source it says where to set one and exits 0; it fails (exit 1) only when every source failed. The summary lists created, updated, cancelled, missing and unchanged.

The sources

Base\Agenda\Service\IcsImporter::getSources(): agenda.sources, agenda.ics (comma-separated: an environment variable) and the address pasted in the back office (the setting agenda.ics). What a source holds is told by its content: an iCalendar file (IcsParser), a Squarespace events collection (SquarespaceParser, six pages back), an agenda feed (FeedParser, its next pages followed, twenty at most). An address is http(s):// or webcal(s)://; anything else is a file.

Google Calendar

  1. In Google Calendar, a calendar of its own for the concerts, shared with the agency if it keeps the dates too.
  2. When the site has dates already: the site's own /agenda.ics imported into it (Settings → Import & export → Import). Each date carries its UID, which the sync matches on, and its fields in the words the sync reads (Event::getDescriptionText()): read back, the site's dates change nothing.
  3. Its Settings and sharing → Integrate calendar → Secret address in iCal format. It needs no OAuth, but whoever holds it reads the whole calendar: keep it out of git.
  4. Paste it on the back office's calendar page (/admin/agenda/calendar): kept in the agenda.ics setting, shown half hidden (or secrets:set AGENDA_ICS and agenda: { ics: '%env(AGENDA_ICS)%' }).
  5. A concert is an event of that calendar: the title is the concert's name, the location the hall and its address, the description one piece of information per line.
Orchestre : NDR Elbphilharmonie Orchester
Chef : Vasily Petrenko
Rôle : soliste
Billets : https://www.elbphilharmonie.de/...
Programme :
Glière — Concerto pour harpe op. 74
Debussy — Danses sacrée et profane

The words of a description

Base\Agenda\Service\DescriptionParser::parse(?string $description) reads them whatever their case and accents, in English, French, German and Italian:

Field Words
ensemble Orchestra, Ensemble, Orchestre, Orchester
conductor Conductor, Chef, Direction, Dirigent, Leitung, Direttore
role Role, Rôle, Rolle, Ruolo - soloist, concertmaster, chamber, orchestra, recital, masterclass in any of the four languages
performers With, Avec, Mit, Con - on the line, comma-separated ("Anna Meyer, harpe" stays one), or one per line under it
tickets Tickets, Billets, Karten, Biglietti - the first address
programme Programme, Program, Programm, Programma - the lines under it, until a blank line or another word

A line no word opens is the programme once a word was read; a description with no word at all is the programme. Google's HTML (bold, line breaks, wrapped links) is read as text. Run in omnisense/omnibase's harness image:

use Base\Agenda\Service\DescriptionParser;

print_r(DescriptionParser::parse("Orchestre : NDR Elbphilharmonie Orchester\nChef : Vasily Petrenko\nRôle : soliste\nBillets : https://www.elbphilharmonie.de/de/programm/123\nProgramme :\nGlière — Concerto pour harpe op. 74\nDebussy — Danses sacrée et profane"));
Array
(
    [ensemble] => NDR Elbphilharmonie Orchester
    [conductor] => Vasily Petrenko
    [role] => soloist
    [performers] => Array
        (
        )

    [tickets] => https://www.elbphilharmonie.de/de/programm/123
    [programme] => Array
        (
            [0] => Glière — Concerto pour harpe op. 74
            [1] => Debussy — Danses sacrée et profane
        )

    [rest] => 
)

The calendar is the source of truth

A date is matched by its entry's UID. At each sync it takes from its calendar the title, the hours, the place - a Venue found by its full address as the site writes it, else by name and town or an alias merged into it, else opened from LOCATION (the name before the first comma, the town in the last part) -, the URL as the organiser's page, STATUS:CANCELLED, and what the description says: a field the description does not mention keeps what the site has; a word with nothing after it ("Chef :") clears it. A house's picture the source gives becomes the photo of a house that has none (once); a date's own picture the cover of a date that has none. A repeating entry gives its first occurrence. A new date goes online at once.

A date whose texts were rewritten on the site is protected by Keep the site's texts (syncLocked): the sync brings its hours and its cancellation, nothing else.

Each change is a Base\Agenda\Model\Change - created, updated (each field as it was and as it becomes, the hours on the hall's clock), cancelled by its calendar, or missing from it. IcsImporter::import(..., apply: false) and sync(apply: false) only tell them: nothing written, no picture fetched. Model\ImportSummary adds them up, with the sources that could not be read (errors).

The dates gone from their calendar

A date that leaves its calendar is neither cancelled nor deleted behind anyone's back: it waits on the calendar page (the setting agenda.sync.missing) until someone says Mark cancelled or Keep on the site (the site's own from then on: no calendar asks about it again; IcsImporter::resolveMissing(array $ids, bool $cancel)). Only the dates to come are checked, per source.

What a sync leaves

IcsImporter::remember(ImportSummary) keeps the last sync (agenda.sync.last: when, the counts, the errors) and, when it changed something, appends it to agenda.sync.history (the last ten, a hundred changes each), for the calendar page. A calendar that could not be read leaves the dates already waiting where they are. A --source or --dry-run run is not kept.

A Squarespace site, during a move

The JSON of a Squarespace events page (https://…/performances?format=json, with or without ?format=json) is read by Service\SquarespaceParser: uid squarespace:<id>, the bold first line of the body as the ensemble, "Name Conductor" / "Name Piano" as the conductor and the performers, "Composer Work" as the programme, the sourceUrl as the organiser's page, the assetUrl as the hall's photo (the cover of a date that names no hall), an end typed a day late as the same evening's, the past dates followed six pages back. Squarespace keeps every hour on the site's clock (Berlin's): the hour typed is set back in the hall's timezone, the country's (addressCountry, else the end of the address).

Another omnibase site

Another site's /agenda.json is a source too: its dates keep their id, so a date read from two places is one.

Reading an ICS file

Base\Agenda\Service\IcsParser::parse(string $ics) gives Model\ParsedEvents (UID, summary, start, end - for whole days the last day -, timezone - the entry's TZID, else agenda.timezone -, description, location, URL, last modified, cancelled, sequence, recurring); the description is read by DescriptionParser when the importer applies it:

use Base\Agenda\Service\IcsParser;

$ics = implode("\r\n", [
    'BEGIN:VCALENDAR', 'VERSION:2.0', 'BEGIN:VEVENT',
    'UID:abc123@google.com',
    'DTSTART;TZID=Europe/Berlin:20261115T200000',
    'DTEND;TZID=Europe/Berlin:20261115T220000',
    'SUMMARY:Perspectives concertantes',
    'LOCATION:Elbphilharmonie\, Platz der Deutschen Einheit 4\, Hamburg',
    'END:VEVENT', 'END:VCALENDAR', '',
]);
foreach ((new IcsParser('Europe/Berlin'))->parse($ics) as $event) {
    echo $event->uid, ' | ', $event->summary, ' | ', $event->start->format(DATE_ATOM), ' | ', $event->timezone, "\n";
    echo 'location: ', $event->location, "\n";
}
abc123@google.com | Perspectives concertantes | 2026-11-15T20:00:00+01:00 | Europe/Berlin
location: Elbphilharmonie, Platz der Deutschen Einheit 4, Hamburg

Proposer une modification docs/usage-sync.md

Structured data (JSON-LD)

Each date's page (agenda_event) carries a schema.org event in a <script type="application/ld+json">, written by Service\JsonLd from the Event: its name, its start and end in ISO 8601 with the hall's offset (a date of whole days as days), eventStatus (EventCancelled when cancelled), the Place and its PostalAddress (and GeoCoordinates when the venue has them), the cover, the tickets as an Offer, the performers - the site's own name first, then each performer, then the ensemble -, and the programme as workPerformed.

The type

# config/packages/agenda.yaml
agenda:
    jsonld: true                  # false: no JSON-LD at all
    jsonld_type: EducationEvent   # MusicEvent (default), EducationEvent, Event
jsonld_type For @type Ensemble Programme
MusicEvent (default) a musician MusicEvent; a date whose role is masterclass: EducationEvent MusicGroup each line "Composer — Work": a CreativeWork with its creator
EducationEvent a lecturer, a teacher trainer, a school EducationEvent PerformingGroup each line a CreativeWork named as written (the talks' titles)
Event anything else Event PerformingGroup idem

An unknown value is refused by the configuration; new JsonLd('…') outside the container falls back to MusicEvent.

Outside the page

use Base\Agenda\Service\JsonLd;

public function __construct(private readonly JsonLd $jsonLd) {}

$data = $this->jsonLd->for($event, $absoluteUrl, $absoluteImageUrl, 'Monica Neagoy');
$this->jsonLd->getType();   // 'EducationEvent'

The fourth argument is the person or group the site is about: the first performer of every date.

Proposer une modification docs/structured-data.md

In a host page

Base\Agenda\Twig\AgendaExtension:

Returns
agenda_upcoming(limit = 3) the next dates, for a home page
agenda_next() the very next one, for a banner
agenda_today() today's dates, each in its own timezone, cancelled ones left out
agenda_google_url(event) Google Calendar's template page for the date
agenda_ics_url(event, absolute = false) the date's .ics
agenda_webcal_url() webcal://…/agenda.ics, the subscription address
agenda_months(events) dates grouped by month
role|agenda_role_label the role's name in the agenda domain

Partials: @Agenda/client/_event.html.twig (a date as the list renders it), _calendar_links.html.twig (the two calendar links), _today.html.twig (today's concert before anything else: "Tonight", the hour, the hall, the part, the orchestra, the tickets; "On stage now" while it plays) and _today_chip.html.twig (the same in one line, for a home page's first screen); both render nothing on a day without a date. Every template is overridden in templates/bundles/AgendaBundle/....

The stylesheet bundles/agenda/css/agenda.css follows the host's custom properties when it defines them - --agenda-accent, --agenda-on-accent, --agenda-ink, --agenda-soft, --agenda-line, --agenda-surface, --agenda-font-display, --agenda-measure - and is neutral otherwise.

Translations: the domains agenda and entities, ICU, in English, French and German.

In the newsletter's digest

With omnibase/newsletter installed, Base\Agenda\Digest\AgendaDigestSource (tagged newsletter.digest_source) gives its digest a part named agenda: each date between the two days, its title, a line - "cancelled" when it is, the day and hour in the letter's language, the venue, the ensemble and conductor -, its page and its start.

Proposer une modification docs/usage-twig.md

The back office

Loaded when omnibase/admin is installed. The two CRUDs carry omnibase/admin's #[OpenToAdmins]: ROLE_ADMIN creates, edits and deletes, and uses their own actions.

Dates

EventCrudController (slug events, newest first): title, state, slug (following the title), start and end, timezone, all day, cancelled, venue, role, ensemble, conductor, programme, performers, tickets, organiser's page, cover, headline, excerpt, text, the source UID (detail), and - for an imported date only - "Keep the site's texts". Filters: state, venue, role, start, cancelled. A new date is in agenda.timezone, owned by the signed-in user.

  • Duplicate (/admin/events/{entityId}/duplicate) - a copy as a draft, " (copy)" in its language after the title, opened for editing: the next date of a tour.
  • Read the calendars now (/admin/events/sync, when there are sources) - the calendar page's preview.

Venues

VenueCrudController (venues, by name): name, hall, address, postcode, town, country, site, map, latitude, longitude, photo and credit, aliases and slug (detail). Filters: town, country. Merge into… (/admin/venues/{entityId}/merge) lists the other houses, the likely same first (names a letter or two apart, the same town counting: 80 or more out of 100), with their dates; posted, the dates move and this house goes (VenueRepository::merge()).

The calendar page

Base\Agenda\Controller\Admin\CalendarController, ROLE_ADMIN, in the admin's layout:

Route Path Method
agenda_admin_calendar /admin/agenda/calendar GET the address (half hidden), the configured calendars, the last sync, the dates gone waiting for a word, the syncs that changed something, the how-to; ?preview=1 reads the calendars now and shows what they would change
agenda_admin_calendar_save the same POST the address saved (webcal://, webcals://, http(s):// only) or forgotten (forget); token agenda-calendar
agenda_admin_calendar_sync /admin/agenda/calendar/sync POST every calendar read and applied, as the cron does; token agenda-sync
agenda_admin_calendar_missing /admin/agenda/calendar/missing POST ids[] and decision=cancel or keep; token agenda-missing

The tile

agenda_upcoming (UpcomingEventsWidgetType): how many dates are to come and the next five, each opening its record.

Proposer une modification docs/back-office.md

Configuration reference

agenda:

Base\Agenda\DependencyInjection\AgendaConfiguration, dumped with Symfony's YamlReferenceDumper in omnisense/omnibase's harness image:

agenda:

    # ICS addresses agenda:sync reads (a Google Calendar's private address, an orchestra's feed).
    sources:              []

    # More ICS addresses, comma-separated, read when the command runs: the place for "%env(AGENDA_ICS)%", which a list cannot take.
    ics:                  null

    # Past dates listed per page.
    past_per_page:        24

    # The timezone of a new date, and of an imported one that names none.
    timezone:             Europe/Berlin

    # The name of the ICS feed; null: the site's title.
    calendar_name:        null

    # A schema.org event on each date's page.
    jsonld:               true

    # Its schema.org type: a musician's MusicEvent (a masterclass stays an EducationEvent), a lecturer's EducationEvent, or Event.
    jsonld_type:          MusicEvent # One of "MusicEvent"; "EducationEvent"; "Event"

    # The past dates under the ones to come.
    show_past:            true
Option Type Default Read by
sources list of addresses [] IcsImporter
ics comma-separated string or null null IcsImporter
past_per_page int, 1 or more 24 AgendaController
timezone timezone Europe/Berlin IcsParser, SquarespaceParser, AgendaController, EventCrudController
calendar_name string or null null AgendaController (ICS and JSON); null: base.settings.title, else "Agenda"
jsonld bool true AgendaController
jsonld_type MusicEvent, EducationEvent, Event MusicEvent JsonLd
show_past bool true AgendaController

Settings

Kept in the core's setting bag by IcsImporter: agenda.ics (the address pasted), agenda.sync.last, agenda.sync.history, agenda.sync.missing.

Services

config/services.php autowires and autoconfigures src/ but DependencyInjection/, Entity/, Enum/, Model/, Controller/Admin/, Admin/, Digest/ and the bundle class; Controller/Client/ is tagged controller.service_arguments; with omnibase/admin, Controller/Admin/ and Admin/; with omnibase/newsletter's DigestSourceInterface, Digest/. SyncCommand is #[AsCommand], SitemapListener #[AsEventListener].

Proposer une modification docs/configuration.md

Testing

vendor/bin/phpunit

Unit tests of the ICS writer and reader, the Google link, the JSON-LD, the JSON feed (written and read back), the description reader, the Squarespace reader (on a real excerpt, tests/Fixtures/squarespace.json) and the Event's own rules; none needs a kernel, but tests/Controller/Admin/OpenToAdminsTest (an administrator writes in the back office, a plain user does not), which runs inside a host application and is skipped elsewhere.

In omnisense/omnibase's harness, this checkout mounted over the package:

docker run --rm --network none -v "$PWD":/srv/demo/vendor/omnibase/agenda:ro omnisense/omnibase-harness test agenda
..............................................................    62 / 62 (100%)

OK (62 tests, 569 assertions)

tests/Service/IcsTest::testEveryPieceComesBackWhereItWas proves the round trip of Google Calendar's step 2: a date written to the site's .ics and read back changes nothing.

Proposer une modification docs/testing.md