omnisense / omnical
Omnical: calendars and public holidays under any PHP host, whoever keeps them - a user's calendars and their events read and written (time zones, recurrence kept as RRULE data, attendees and their answers, ETags against lost updates), busy time and the free spans everyone shares, a country's public
Requires
- php: >=8.2
- ext-mbstring: *
- omnisense/contracts: ^1.0@dev
- symfony/http-client-contracts: ^3.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
- symfony/http-client: ^6.4|^7.0|^8.0
Suggests
- omnical/caldav: CalDAV (RFC 4791): iCloud, Fastmail, Nextcloud, any sabre/dav server - calendars, events read and written, free/busy
- omnical/google: Google Calendar (API v3): calendars, events read and written, free/busy, over OAuth 2.0
- omnical/microsoft: Microsoft Graph: Outlook and Microsoft 365 calendars, events read and written, schedules (free/busy), over OAuth 2.0
- omnical/nager: Nager.Date: public holidays of 200+ countries, no key (commercial use needs a sponsorship) - a declared provider
- omnical/openholidays: OpenHolidays API: public and school holidays of 36 countries by region, no key, open data (ODbL) - a declared provider
- omnisense/omnibridge: The host contracts; omnibridge/symfony and omnibridge/laravel wire omnical into their host when a site needs it
- symfony/http-client: The HTTP client a declared provider uses when none is given
- symfony/yaml: Provider definitions written in YAML (JSON needs nothing)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-10 12:34:05 UTC
README
Calendars and public holidays under any PHP host, whoever keeps them: one family in three contracts.
| Contract | What it answers |
|---|---|
CalendarEventsInterface (calendar-events) |
a user's calendars and their events, read and written: time zones, recurrence kept as RRULE data (each occurrence on its own when asked), attendees and their answers, ETags against lost updates |
FreeBusyInterface (free-busy) |
when calendars, or people, are busy; FreeBusy::common() gives the spans free in all of them |
HolidaysInterface (holidays) |
a country's public (and school) holidays, by region and type, in the language asked |
| Provider | Contracts | Written as | Key |
|---|---|---|---|
| omnical/google | calendar-events, free-busy - Google Calendar API v3 | code | OAuth 2.0 |
| omnical/microsoft | calendar-events, free-busy - Microsoft Graph (Outlook, Microsoft 365) | code | OAuth 2.0 |
| omnical/caldav | calendar-events, free-busy - CalDAV (RFC 4791): iCloud, Fastmail, Nextcloud, any sabre/dav server | code | a password (an app password) or a token |
| omnical/openholidays | holidays - 36 countries, public and school holidays, ODbL | data (provider.json) |
none |
| omnical/nager | holidays - 200+ countries; commercial use needs a sponsorship | data (provider.json) |
none |
use Omnical\Declarative\DeclarativeFactory; use Omnical\Model\{EventQuery, FreeBusy, FreeBusyQuery, HolidayQuery}; use Omnical\Registry; $calendars = new Registry([new GoogleFactory($http, $tokens), ...DeclarativeFactory::installed($http)], [ 'agenda' => ['factory' => 'google', 'options' => ['client_id' => '…apps.googleusercontent.com', 'client_secret' => 'GOOGLE_CLIENT_SECRET', 'token' => 'user:42:google']], 'holidays' => ['factory' => 'openholidays', 'options' => ['user_agent' => 'MySite/1.0 (https://example.org; contact@example.org)']], ]); $week = $calendars->calendarEvents('agenda', 'user-42')->events(new EventQuery('primary', $monday, $saturday)); $busy = $calendars->freeBusy('agenda', 'user-42')->freeBusy(new FreeBusyQuery(['primary', 'colleague@example.org'], $monday, $saturday)); $slots = FreeBusy::common($busy, $monday, $saturday, 3600); // an hour free for both $closed = $calendars->holidays('holidays')->holidays(HolidayQuery::year('FR', 2027, 'FR-GE-BR', 'fr'));
A provider described as data
omnical/openholidays and omnical/nager hold no PHP: a definition file (base URI, auth by
reference, endpoints, how each answer's fields map onto Holiday, the policy beside it), loaded
at run time by Declarative\DeclarativeFactory. A site may write its own the same way.
docs/declarative.md says what fits (plain REST JSON reads) and what does
not (OAuth consent, writes, webhooks, XML), and proposes the mechanism's move to
omnisense/contracts once a second family needs it.
Usage policies, secrets, OAuth
Each provider declares its usage policy as data (policy.json: rate limits, rights, attribution,
requirements, where and when it was read), and every call through calendarEvents(),
freeBusy() or holidays() is held to it: checked before, counted after (per user where the
provider counts per user: pass the site's user to the accessor), refused with
UsageLimitReachedException once a limit is reached or while the provider asks to wait. A site
tightens a policy in its configuration (policy), says what it is (usage), never loosens it.
Secrets go by reference: options name a secret (GOOGLE_CLIENT_SECRET), Auth\SecretsInterface
reads it at the moment of the call (EnvSecrets by default, the host's vault otherwise). OAuth
2.0 is Auth\OAuth2Client (authorization code, PKCE, refresh) with a TokenStoreInterface the
application keeps; RefreshingAccessToken refreshes before expiry and once on a 401.
docs/auth.md, docs/policies.md.
omnisense/omnical requires omnisense/contracts (the kernel every omnisense family builds on)
and symfony/http-client-contracts, nothing else: no framework, no HTTP client of its own (each
provider package requires what it calls and takes a client from outside).
Documentation
- docs/index.md: the manual's contents
- docs/installation.md: plain PHP first
- docs/models.md, docs/providers.md, docs/declarative.md
- docs/auth.md, docs/policies.md, docs/errors.md
- docs/testing.md, docs/harness.md, docs/writing-a-provider.md, docs/symfony.md
Boundaries: omnical reads and writes calendars and holidays; omnibase/agenda publishes a site's
own dates and ICS feeds; omnimeet opens the room an appointment leads to.
Licence
MIT