The press kit for the sites built on omnisense/omnibase:
what an artist's agency - or a presenter's programme editor - asks a site for, on one page,
/press. It is a Symfony bundle (Base\Press\PressBundle) on the core 3 or 4, with its back
office on omnibase/admin. It was first made for two musicians' sites and holds nothing that is
theirs alone.
- A
Quoteis what was said or written: the words, who said them, where, what about, its kind (press, colleague, audience), with its translations beside the original words. - A
Photois the original as the photographer delivered it (64 MB at most), its credit, a caption, whether the original may be downloaded, and where it is shown: the press kit, the site's own gallery, or both. - A
Biois the biography once per language and per length:short(about 100 words),medium(about 250),long. - The contacts - the agency, the label's press office - are configuration.
Use it for a performer's, a speaker's or an author's site. It is not a press-release mailer.
How this manual is organised
- Installation: requirements, the bundle, the routes, the schema, the layout.
- Quotes, photos and biographies: the entities, their fields and methods, the queries, an in-memory example.
- The page and the downloads:
/press, the download of an original, the languages, the Twig functions, the templates and the stylesheet. - The back office: the three screens, who writes, the feature button, the word counter.
- Configuration reference:
press:, the parameters, the services. - Testing: the suite and what it checks.
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, and
symfony/config, dependency-injection, form, http-kernel, security-bundle and
string 6, 7 or 8. The back office's screens use omnibase/admin's #[OpenToAdmins]
attribute (omnibase/admin from 7474f85).
The bundle
composer require omnibase/press:dev-main
// config/bundles.php
return [
// ...
Base\BaseBundle::class => ['all' => true],
Base\Admin\AdminBundle::class => ['all' => true],
Base\Press\PressBundle::class => ['all' => true],
];
PressBundle extends the core's AbstractBaseBundle: its root is the package's
(templates/ is @Press, public/ is bundles/press/), and build() aliases
Base\Press\Entity\* to App\Entity\Press\* and the repositories to App\Repository\Press\*:
an application takes an entity over by declaring App\Entity\Press\Quote extends Base\Press\Entity\Quote.
The routes
# config/routes.yaml
press_controller:
resource: "@PressBundle/src/Controller/Client"
type: attribute
prefix: /
| Route | Path | Methods | |
|---|---|---|---|
press_index |
/press |
GET | the page; in the sitemap (0.6, monthly) |
press_photo_download |
/press/photo/{id}/download |
GET | the original as an attachment |
Configuration
# config/packages/press.yaml (every key optional)
press:
download_kit: true
bio_lengths: [short, medium, long]
contacts:
- name: "Anna Keller"
role: "General management"
organisation: "Keller Artists"
email: "anna@keller-artists.example"
phone: "+49 30 000 00 00"
url: "https://keller-artists.example"
The schema and the public files
bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
bin/console assets:install
Tables press_quote, press_photo, press_bio; the stylesheet bundles/press/css/press.css.
What the layout provides
The page extends layout1.html.twig and fills title, description, content and
stylesheets.
Proposer une modification docs/installation.md
Quotes, photos and biographies
Quote
Base\Press\Entity\Quote (press_quote, indexed on visible, featured, position):
| Property | Column | Meaning |
|---|---|---|
text |
text, not blank |
the words; the quotation marks typed around them are dropped (the template adds its own) |
language |
string(5), nullable |
the language the words were said in - what "the original" is; falls back on locale |
translations |
json, nullable |
per two-letter language: {text, role} - the words translated, the speaker's quality |
offerOriginal |
boolean (offer_original), true |
whether a translated quote lets the reader see the original |
locale |
string(5), nullable |
restricts the quote to the readers of one language; null: shown to all |
author |
string(160), nullable |
who speaks ("Joshua Bell"); empty when only the paper signs |
role |
string(160), nullable |
what the author is ("violinist") |
source |
string(160), nullable |
the publication |
url |
string(500), nullable |
its link (a URL with a TLD) |
date |
date_immutable (quoted_on), nullable |
|
kind |
QuoteKind, PRESS |
PRESS, COLLEAGUE, AUDIENCE; an unknown value is PRESS |
featured |
boolean |
among the few a home page shows |
position |
integer |
|
release |
string(200) (about_release), nullable |
what it is about: an album's title, a concert - free words |
visible |
boolean, true |
Empty strings are kept as null. Methods:
getTextIn(string $locale),getRoleIn(string $locale)- the translation for a reader of that language, else the original words and role.isTranslatedIn($locale)- there is a translation and it is not the original's language.setTranslation(string $locale, ?string $text, ?string $role = null)- nothing in either removes it;setTranslations(array)takes{fr: {text, role}}or{fr: 'words'}.- The magic properties
translation_<xx>androle_<xx>read and writetranslations- the back office's fields, one pair per language of the site. speaks(string $locale)- nolocale, or that one.getSignature()- "Joshua Bell, violinist — BBC Music Magazine"; the source is not repeated when the role or author already names it.toggleFeatured().
Photo
Base\Press\Entity\Photo (press_photo, indexed on visible, position):
| Property | Column | Meaning |
|---|---|---|
file |
upload, image, 64 MB | the original |
title |
string(160), nullable |
|
credit |
string(160), nullable |
the photographer, as a paper must print it |
caption |
text, nullable |
|
downloadable |
boolean, true |
the original may be taken |
position |
integer |
|
visible |
boolean, true |
in the press kit, on /press |
gallery |
boolean, false |
in the site's own gallery (press_gallery()) |
A photograph not cleared for the press goes in the gallery only. getFileUrl() is the
address an <img> takes (/uploads/...: the storage's path with the public directory taken
off). getAlt() is the caption, else the title. getCreditLine() adds © unless the credit
starts with ©, (c), photo or foto.
Bio
Base\Press\Entity\Bio (press_bio, unique on locale and bio_length):
| Property | Column | Meaning |
|---|---|---|
locale |
string(5) |
lowercased; en when empty |
length |
BioLength (bio_length) |
SHORT (100 words), MEDIUM (250), LONG (no limit); an unknown value is SHORT |
content |
text, not blank |
plain text, a blank line between two paragraphs; line ends normalised, trimmed |
updatedAt |
datetime |
set on create and update (the core's #[Timestamp]): an agency sees the biography is current |
getParagraphs(), getWordCount(), Bio::countWords(string) - what stands between two
blanks, in any alphabet: "l'archet" and "Sinfonie-Orchester" are one word each, a lone dash
none. isTooLong() - more than the length's words plus ten percent (never for LONG).
In memory
Run in omnisense/omnibase's harness image, with this checkout's src/ on the autoloader:
use Base\Press\Entity\Bio;
use Base\Press\Entity\Photo;
use Base\Press\Entity\Quote;
use Base\Press\Enum\BioLength;
use Base\Press\Service\PhotoFilename;
$quote = (new Quote('« A harp that sings. »', 'Joshua Bell', 'BBC Music Magazine'))->setRole('violinist')->setLanguage('en');
$quote->setTranslation('fr', 'Une harpe qui chante.', 'violoniste');
echo $quote->getText(), ' | ', $quote->getSignature(), "\n";
echo $quote->getTextIn('fr'), ' | ', $quote->getRoleIn('fr'), ' | ', $quote->getTextIn('de'), "\n";
$bio = new Bio('FR', BioLength::SHORT, str_repeat('mot ', 111));
var_dump($bio->getLocale(), $bio->getWordCount(), $bio->isTooLong());
echo Bio::countWords("l'archet du Sinfonie-Orchester - fin"), "\n";
echo (new Photo())->setCredit('Harald Hoffmann')->getCreditLine(), "\n";
echo PhotoFilename::of('Clara Weiss', 'Portrait with harp', '© Harald Hoffmann', 12, 'JPEG'), "\n";
echo PhotoFilename::of('clara-weiss.example', null, null, 12, 'png'), "\n";
A harp that sings. | Joshua Bell, violinist — BBC Music Magazine
Une harpe qui chante. | violoniste | A harp that sings.
string(2) "fr"
int(111)
bool(true)
4
© Harald Hoffmann
clara-weiss-portrait-with-harp.jpg
clara-weiss-example-photo-12.png
Repositories
| Repository | Methods |
|---|---|
QuoteRepository |
findVisible(?string $locale = null, ?QuoteKind $kind = null, ?int $limit = null, bool $featuredOnly = false) - visible, of that language or none, featured first, then by position, newest first; findForLocale(string $locale, string $fallback = 'en', ...) - the same, falling back on English when the reader's language has nothing |
BioRepository |
findOneFor(BioLength $length, string $locale, string $fallback = 'en'); findByLength(string $locale, array $lengths, string $fallback = 'en') - length => biography, a length nobody wrote left out |
PhotoRepository |
findVisible(?int $limit = null) (the press kit), findGallery(?int $limit = null) (the site's gallery), by position |
Proposer une modification docs/concepts.md
The page and the downloads
/press
Base\Press\Controller\Client\PressController::index() renders @Press/client/index.html.twig:
| Variable | Content |
|---|---|
quotes |
the featured quotes of the reader's language (else English); a site that featured none shows them all |
bios |
press.bio_lengths in order, each in the reader's language or English, each with its "copy" button; a length nobody wrote is left out |
photos |
the press kit's pictures (visible), their credit, and their original to download when allowed |
contacts |
press.contacts |
download_kit |
press.download_kit |
The languages: a quote is for a reader of its locale, or of every language when it has none;
its words are the reader's translation when there is one (with the original on demand when
offerOriginal). The fallback language is PressController::FALLBACK_LOCALE, en.
Downloading an original
GET /press/photo/{id}/download sends the original as an attachment, with
X-Robots-Tag: noindex. A 404 when the picture does not exist, is not in the press kit, is not
downloadable, has no file, or press.download_kit is off.
The file is named by Base\Press\Service\PhotoFilename::of($site, $title, $credit, $id, $extension):
<site>-<slug>.<ext> - the site's title (base.settings.title, else the host without www.),
the picture's title, else its credit without its sign, else photo-<id>; slugged in ASCII and
lowercased; jpeg written jpg; an extension cannot carry a path. The file in a journalist's
folder still says whose it is.
Twig functions
Base\Press\Twig\PressExtension, in the current request's language:
| Function | Returns |
|---|---|
press_quotes(limit = 3, kind = null) |
the visible quotes, featured first, falling back on English; kind: press, colleague, audience |
press_bio(length = 'short', locale = null) |
one biography (.content, .paragraphs, .wordCount, .updatedAt), in the language or English, or null |
press_photos(limit = null) |
the press kit's pictures (.fileUrl, .alt, .caption, .creditLine) |
press_gallery(limit = null) |
the pictures of the site's gallery, in the press kit or not |
press_contacts() |
press.contacts |
{% for quote in press_quotes(2) %}
{% include '@Press/client/_quote.html.twig' with {quote: quote} %}
{% endfor %}
{% set bio = press_bio('medium') %}
{% if bio %}{% for paragraph in bio.paragraphs %}<p>{{ paragraph }}</p>{% endfor %}{% endif %}
Templates and look
@Press/client/: index, _quote (one quote), _quote_words. An application overrides them in
templates/bundles/PressBundle/client/.... bundles/press/css/press.css follows the host's
--press-accent, --press-ink, --press-soft, --press-line, --press-surface and
--press-measure when it defines them.
Translations: the domain press (ICU), in English, French and German.
Proposer une modification docs/usage-pages.md
The back office
Loaded when omnibase/admin is installed. The three screens carry omnibase/admin's
#[OpenToAdmins]: the site's administrator (ROLE_ADMIN) creates, edits and deletes, not only
the super-administrator - and, on quotes, uses the feature button.
| CRUD | Slug | Fields |
|---|---|---|
QuoteCrudController |
quotes |
text, author, role, source, link, date, about, kind, restricted to a language, the original's language, offer the original, then per language of the site a translation and the speaker's quality, position, featured, visible; filters kind, featured, language |
PhotoCrudController |
photos |
the original, title, credit, caption, position, downloadable, in the press kit, in the gallery; filters downloadable, visible, gallery |
BioCrudController |
bios |
language, length, words (list), updated (list), text; filters language, length |
The languages are kernel.enabled_locales, else en, fr, de.
Feature (/admin/quotes/{entityId}/feature, on the list and on a quote) toggles featured
and says which way it went.
The biography's text has a live word counter under it: the same words Bio::countWords()
counts, shown against 100 or 250 for the length chosen, in red past ten percent more.
Proposer une modification docs/back-office.md
Configuration reference
press:
Base\Press\DependencyInjection\PressConfiguration, dumped with Symfony's YamlReferenceDumper
in omnisense/omnibase's harness image:
press:
# Who the press writes to: the agency, the label's PR, the artist.
contacts:
# Prototype
-
name: ~ # Required
# "General management", "Press, Germany"...
role: null
email: null
phone: null
organisation: null
url: null
# The HD downloads at all: off, the photos are shown and none can be taken.
download_kit: true
# The biographies the page shows, in this order.
bio_lengths:
# Defaults:
- short
- medium
- long
| Option | Type | Default | Effect |
|---|---|---|---|
contacts |
list of {name, role, email, phone, organisation, url} |
[] |
the page's contacts and press_contacts(); name is required and not empty |
download_kit |
bool | true |
false: the pictures are shown, none can be downloaded |
bio_lengths |
list of short, medium, long |
all three | the biographies /press shows, in this order |
The parameters are press.contacts (read whole), press.download_kit, press.bio_lengths
(and the flattened leaves of each).
Services
config/services.php autowires and autoconfigures every class of src/ but
DependencyInjection/, Entity/, Enum/, Controller/Admin/ and the bundle class;
Controller/Client/ is tagged controller.service_arguments; with omnibase/admin,
Controller/Admin/ too. PressController and PressExtension read the parameters with
#[Autowire]; PressController reads base.settings.title from the core's setting bag.
Proposer une modification docs/configuration.md
Testing
vendor/bin/phpunit # checked out on its own
php vendor/bin/phpunit -c vendor/omnibase/press/phpunit.xml.dist # from an application
In omnisense/omnibase's harness, this checkout mounted over the package (the harness registers the press bundle for this suite):
docker run --rm --network none -v "$PWD":/srv/demo/vendor/omnibase/press:ro omnisense/omnibase-harness test press
.................................. 34 / 34 (100%)
OK (34 tests, 142 assertions)
| File | What |
|---|---|
Entity/QuoteTest |
quotation marks typed around the text go; a default quote is a press quote, online, not featured; the feature button toggles it; the kind from its value, an unknown one is press; the signature names author, role and source once; a quote with no locale speaks to everyone, a restricted one to its language; empty strings kept as nothing; the date kept immutable; as a string, its first words; a reader gets the words in their language and may ask for the original |
Entity/BioTest |
words are what stands between two blanks; a biography's count is its content's; paragraphs; trimmed content, an empty one is nothing; a short biography too long past 110 words; each length's words; the locale lowercased, an unknown length is short |
Service/PhotoFilenameTest |
site, then title, then extension; accents and signs spelled out; without a title the credit without its sign; with neither, its number; an extension cannot carry a path |
Controller/Admin/OpenToAdminsTest |
an administrator writes in the back office, someone else is refused (inside a host application, skipped elsewhere) |
Controller/CrudRequiredFieldsTest |
the CRUDs' required fields |