Aller au contenu

omnibase/press

Press for omnibase: quotes, hi-res photos with credits, biographies in three lengths - the press kit page an artist’s agency asks for.

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

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 Quote is 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 Photo is 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 Bio is 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

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> and role_<xx> read and write translations - the back office's fields, one pair per language of the site.
  • speaks(string $locale) - no locale, 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

Proposer une modification docs/testing.md