Social publishing for the sites built on omnisense/omnibase:
a reel made from the site's own template - its logo, a band with a name on it, its colours,
its font, a clip before and after - sent to each network with what it says there, and the
account's feed kept for a wall on the site. It is a Symfony bundle (Base\Social\SocialBundle)
on the core 3 or 4; the networks are omnisense/omnipost's
providers (instagram, youtube...). This bundle knows the site - its templates, its posts,
its settings -, omnipost knows the networks.
A violinist's site posts a concert's excerpt to Instagram and YouTube from its back office; a shop shows its last posts under its products.
The pipeline, carried by three Messenger messages:
- Render - each network's film made by ffmpeg with its template (the network's own
target's, else the post's, else the network's, else the site's), under
<social.storage>/<post id>/<platform>.mp4, its cover beside it. - Publish - the post as that network receives it, checked by omnipost's
Validatoragainst the network's capabilities and the provider's own rules, then handed over; at the post's hour when it has one to come. - Check - the network's status read again until it says the post is online (its permalink kept) or why it is not.
How this manual is organised
- Installation: requirements, the bundles, ffmpeg, the routes, Messenger, the default URI, the schema.
- Posts, targets and templates: every entity and its states, the queries.
- The pipeline: rendering, the ffmpeg command, checking, publishing, the status checks, retries, the public address of a film.
- Accounts and the wall: connecting a network, the tokens in the vault, the feed, the commands, the Twig functions.
- The back office: the screens, the preview, the accounts page, the tile.
- Configuration reference:
social:, the parameters, the services. - Testing: the suite and what it checks.
Propose a change 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,
omnisense/omnipost 1.x, and symfony/config, dependency-injection, form, http-kernel,
security-bundle, process and messenger 6, 7 or 8. Install the omnipost providers the site
publishes to (omnipost/instagram, omnipost/youtube...).
ffmpeg and ffprobe belong in the image of whatever renders - the Messenger worker, and the web image for the preview's checks of a post not yet rendered - with a font for the band:
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg fonts-dejavu-core && rm -rf /var/lib/apt/lists/*
# alpine: apk add --no-cache ffmpeg font-dejavu
Without them a rendering fails with a RenderingException saying so, shown on the target.
The bundles
composer require omnibase/social:dev-main omnipost/instagram:1.x-dev
// config/bundles.php
return [
// ...
Base\BaseBundle::class => ['all' => true],
Base\Admin\AdminBundle::class => ['all' => true],
Omnipost\Bridge\Symfony\OmnipostBundle::class => ['all' => true],
Base\Social\SocialBundle::class => ['all' => true],
];
SocialBundle aliases Base\Social\Entity\* to App\Entity\Social\* and the repositories to
App\Repository\Social\* (the core's convention: an application declares its own class there to
take one over); its templates/ is @Social, its public/ bundles/social/.
omnipost's configuration holds the apps' ids and secrets; the tokens are the back office's:
# config/packages/omnipost.yaml
omnipost:
providers:
instagram: { factory: instagram, options: { app_id: '%env(default::INSTAGRAM_APP_ID)%', app_secret: '%env(default::INSTAGRAM_APP_SECRET)%' } }
youtube: { factory: youtube, options: { client_id: '%env(default::YOUTUBE_CLIENT_ID)%', client_secret: '%env(default::YOUTUBE_CLIENT_SECRET)%', channel_id: '%env(default::YOUTUBE_CHANNEL_ID)%' } }
# config/packages/social.yaml
social:
providers: [instagram, youtube]
The routes
# config/routes.yaml
social_controller:
resource: "@SocialBundle/src/Controller/Client"
type: attribute
prefix: /
social_admin:
resource: "@SocialBundle/src/Controller/Admin/AccountsController.php"
type: attribute
prefix: /
The first imports social_media (the films' public address); the second the accounts page and
the OAuth callback /connect/social/{provider} (social_oauth_callback) - declare that address
in each network's app.
Messenger
# config/packages/messenger.yaml
framework:
messenger:
routing:
Base\Social\Message\RenderPostMessage: async
Base\Social\Message\PublishTargetMessage: async
Base\Social\Message\CheckTargetMessage: async
A rendering takes a while and a network processes a reel for a minute or two. The async
transport must take delays (Doctrine, Redis and AMQP do; sync:// runs the checks at once and
gives up early): scheduled posts and status checks are DelayStamps.
The site's public address
A worker has no request to take the host from: set framework.router.default_uri to the site's
public address, and keep /social/media/ reachable from the internet (no access restriction,
maintenance or launch page on it) - Instagram comes for the film there.
The schema and the public files
bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
bin/console assets:install
Tables social_post, social_post_target, social_template, social_media; the stylesheet
bundles/social/css/social.css.
The crons
0 * * * * bin/console social:sync # the walls' posts, hourly
30 4 * * * bin/console social:refresh-tokens # tokens that die within ten days
Propose a change docs/installation.md
Posts, targets and templates
SocialPost
What goes out (social_post):
| Property | Column | Meaning |
|---|---|---|
video |
upload, video, 1 GB | the film as it was shot |
image |
upload, image, 8 MB | or a picture |
kind |
string(16), reel |
an omnipost PostKind value: reel or image in the back office |
caption |
text |
|
title |
string(255) |
for the networks that want one (YouTube) |
tags |
json |
hashtags, kept without #, blanks or doubles (SocialPost::cleanTags()) |
template |
many-to-one Template, SET NULL |
|
useTemplate |
boolean, true |
off: the film goes out as it is, not encoded again |
coverSecond |
float, 0 |
the frame the cover is taken at |
scheduledAt |
datetime, nullable |
the hour it leaves; the site holds the message back until then |
link |
string(255) |
a URL |
state |
PostState |
read from its targets |
targets |
one-to-many SocialPostTarget, cascade, orphan removal |
one per network |
refreshState(): no enabled target DRAFT; one sent or processing PUBLISHING; all published
DONE; one failed (and none on its way) FAILED; some published PUBLISHING; all rendered
READY; else DRAFT. PostState also has RENDERING, set while it renders.
toPost(?callable $urls = null): Omnipost\Model\Post gives omnipost's canonical post (the first
network's rendering as its media) and, per enabled network, a Variant with the target's own
caption, title, hashtags, rendering, cover and options. $urls gives each target's public
address (fn (SocialPostTarget $target, bool $cover): string); without it the local path stands
in - enough to validate, not to publish. The hour is not passed on to the networks.
SocialPostTarget
A post on one network (social_post_target, unique on post and platform):
| Property | Meaning |
|---|---|
platform |
an omnipost Platform value - and the name of the provider that serves it |
enabled |
whether it goes there |
caption, title, tags, template |
what it says there when not what the post says; empty: the post's |
options |
the network's extras: share_to_feed (Instagram), privacy (public, unlisted, private: YouTube) |
renderedPath, coverPath, duration, width, height, size |
what the renderer made |
publicationId, permalink, publishedAt |
what the network said |
state |
TargetState |
error |
why it failed |
attempts |
looks at the network's status so far |
updatedAt |
when the state last moved |
TargetState: PENDING, RENDERED, SENT, PROCESSING, PUBLISHED, FAILED; isFinal()
(published or failed), isInFlight() (sent or processing). rendered(Rendering) records a
rendering and clears what a previous publication left; fail(string);
isMediaOpen(int $ttl = 86400) - the public address answers while the publication is on its
way and $ttl seconds after it ended; toMedia() - the rendering as an omnipost Media.
Template
The site's identity on a reel (social_template):
| Property | Default | Meaning |
|---|---|---|
name |
not blank | |
default (is_default) |
false |
the site's own; saving one as default takes the flag off the others of the same platform |
platform |
null |
a template for that network alone |
logo |
a picture (8 MB) | |
logoPosition |
TOP_RIGHT |
TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, CENTER |
logoScale |
0.2 |
the logo's width as a part of the frame's, 0.1 to 0.5 |
band |
false |
a band across the lower third |
bandText |
its text (the artist's name), 120 at most | |
bandColor, textColor |
#000000, #ffffff |
#rrggbb or #rrggbbaa |
font |
a TTF or OTF (8 MB); none: social.ffmpeg.font |
|
background |
#000000 |
behind a film shown whole |
intro, outro |
clips before and after (200 MB) | |
fit |
COVER |
COVER: cropped to fill 9:16; CONTAIN: whole, letterboxed |
fadeSeconds |
0.5 |
fades in and out, 0 to 5 |
TemplateRepository::resolve(SocialPostTarget) - the template a target is rendered with: none
when the post says "as it is", else the target's, the post's, the network's
(findForPlatform()), the site's (findDefault(): default with no platform, else any default).
SocialMedia
One of the account's own posts as the last sync read it - what the wall shows
(social_media, unique on provider and remote id): provider, remoteId, kind, permalink,
caption, publishedAt, thumbnail (a local copy, 8 MB: the networks' own addresses expire),
mediaUrl, videoUrl (the network's, they may expire), likes, comments, hidden (kept off
the wall), fetchedAt.
SocialMediaRepository: findWall(int $limit = 12, ?string $provider = null) (not hidden,
newest first), findOneRemote(), lastSync(string $provider), countFor().
SocialPostTargetRepository::findLastFor(string $platform) - the last target that moved.
Propose a change docs/concepts.md
The pipeline
Base\Social\Service\Publisher holds the logic; the three handlers only call it.
Rendering
RenderPostMessage($postId, $publish = false) → Publisher::render(): the post RENDERING,
then each enabled target not yet published rendered by Base\Social\Service\Renderer::render()
with TemplateRepository::resolve($target). A rendering that fails (RenderingException) fails
its network alone, the message kept as its error. The post's state is then read from its
targets; with $publish, the rendered targets are handed over.
Renderer::render(SocialPost $post, ?Template $template = null, ?Platform $platform = null): Rendering:
- a picture is measured (ffprobe) and given back as it is;
- without a template (the post says "as it is", or the site has none) the film is measured and given back - no second encoding -, its cover taken;
- otherwise ffmpeg makes
<storage>/<post id>/<platform>.mp4and its cover<platform>.jpg(the frame atcoverSecond, at most 0.1 s before the end). A cover that fails does not stop the film (the network takes a frame itself) unless ffmpeg is missing.
probe() reads ffprobe's JSON (Renderer::measures()): the first video stream's size - turned
for a phone's portrait film stored lying down (rotation 90 or 270) -, the length, the weight,
whether there is sound, the MIME type.
The ffmpeg command
Renderer::command(array $input): list<string> only assembles the arguments - no file is
touched - so the filter graph is read and tested as it is. One -filter_complex:
- the source to 1080 x 1920:
COVERscales up and crops,CONTAINscales down and pads on the background;setsar=1,fps=<social.ffmpeg.fps>; - the logo scaled to its part of the width (even), laid with
overlay64 px from the edges; - the band:
drawboxat y 1380, 140 px high, thendrawtextcentred,expansion=none, the template's font orfont=Sans, size 56 shrinking for a long text (28 at least,Renderer::fontSize()); texts and paths escaped for the option and the graph (Renderer::escape()); fadeandafadein and out (none when longer than half the film); a film without sound gets a silence as long as it;- the intro and outro brought to the same frame, rate and sound, joined with
concat; - H.264 High 4.1, yuv420p,
-r <fps>,-crf <social.ffmpeg.crf>, AAC 160k at 48 kHz,+faststart- what Instagram, YouTube, Facebook and TikTok all take.
The filter graph of a cropped film with a logo, a band and fades, run in omnisense/omnibase's
harness image (new Renderer(), its defaults):
use Base\Social\Enum\Fit;
use Base\Social\Enum\LogoPosition;
use Base\Social\Service\Renderer;
$command = (new Renderer())->command([
'source' => '/in.mov', 'output' => '/out.mp4', 'duration' => 12.0,
'fit' => Fit::COVER, 'logo' => '/logo.png', 'logoPosition' => LogoPosition::TOP_RIGHT, 'logoScale' => 0.2,
'band' => true, 'bandText' => 'Clara Weiss: harp', 'fade' => 0.5,
]);
$i = array_search('-filter_complex', $command, true);
echo str_replace(';', ";\n", $command[$i + 1]), "\n";
echo implode(' ', array_slice($command, $i + 2)), "\n";
[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,fps=30[base];
[1:v]scale=216:-1[logo];
[base][logo]overlay=W-w-64:64[logod];
[logod]drawbox=x=0:y=1380:w=1080:h=140:color=0x000000:t=fill,drawtext=font=Sans:text=Clara Weiss\\: harp:expansion=none:fontcolor=0xffffff:fontsize=56:x=(w-text_w)/2:y=1380+(140-text_h)/2,fade=t=in:st=0:d=0.5,fade=t=out:st=11.5:d=0.5,format=yuv420p[v];
[0:a]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,afade=t=in:st=0:d=0.5,afade=t=out:st=11.5:d=0.5[a]
-map [v] -map [a] -c:v libx264 -profile:v high -level 4.1 -pix_fmt yuv420p -r 30 -crf 20 -c:a aac -b:a 160k -ar 48000 -movflags +faststart /out.mp4
Checking
Publisher::validate(SocialPostTarget $target): list<Omnipost\Model\Violation> - what the
network would refuse, before anything is sent: the post as that network receives it
(SocialPost::toPost()->for($platform)) against the provider's capabilities()
(omnipost's Validator), and against the provider's own validate() when it has one (a reel is
9:16 on Instagram). A target not rendered yet is checked as it will be: the source's measures
and length, the template's 1080 x 1920 frame. A provider that cannot be built, or does not
publish, is a violation. The same complaint is given once.
Publishing
Publisher::dispatchPublish(SocialPost $post) sends a PublishTargetMessage per rendered target,
with a DelayStamp up to scheduledAt when it is to come (the networks' own scheduling is not
used). Publisher::publish(SocialPostTarget $target):
- a disabled target, one on its way or published is left;
- no rendering:
FAILED, "render the post first"; - violations:
FAILEDwith the reasons, nothing sent; - otherwise the provider's
publish()with the public addresses; the network's id kept and its answer applied -PUBLISHED(the permalink and date),PROCESSING,SENT(pending) orFAILED(its reason). An omnipost exception fails the target; anUnauthorizedExceptionadds "connect the account again (Social › Accounts)". - On its way, a
CheckTargetMessagefollows aftersocial.check.delayseconds.
Status checks
Publisher::check(SocialPostTarget $target): bool reads the status again for a target on its way:
applied as above; a network that does not answer once is asked again (logged); an unauthorised
answer fails it. After social.check.attempts looks still processing, it fails ("Still
processing on Instagram after 20 looks: see the account itself"). Again on its way: another
CheckTargetMessage after the delay.
Retrying
Publisher::retry(SocialPostTarget $target) - its attempts and error cleared, rendered again
when its film is gone (else RENDERED), and a PublishTargetMessage sent.
The public address of a film
Instagram takes no upload: it is given an address and comes for the film.
Base\Social\Service\MediaUrls::for(SocialPostTarget $target, bool $cover = false) gives
/social/media/{target}/{token}.{ext} (social_media), absolute, signed by
Base\Social\Service\MediaToken: the first 32 hex characters of an HMAC-SHA256 of the target's
id and the file's path with the kernel secret - it names that one file, and a file rendered again
gets another.
MediaController answers it (GET, HEAD, ranges, Cache-Control: private, max-age=3600,
X-Robots-Tag: noindex, nofollow) only when the token names the target's rendering or cover,
the extension is the file's, the file exists, and the publication is on its way or ended less
than social.media_url_ttl seconds ago; anything else is the same 404.
Propose a change docs/usage-publishing.md
Accounts and the wall
The accounts
Base\Social\Service\Accounts keeps the site's accounts in the core's settings, in the vault
when the setting bag can (SettingBag::secure()), as the back office's API keys are:
| Setting | Holds |
|---|---|
api.social.<provider>.token |
the token, an Omnipost\Model\Token::toArray() as JSON |
api.social.<provider>.account |
who it is: id, username, name, url, picture, followers, posts, kind, when read |
api.social.<provider>.api_key |
a reading key, for the providers that take one (YouTube's Data API key) |
names()-social.providersthat omnipost declares, in order;has($name).provider($name)- omnipost'sRegistry::create()with the settings as overrides:access_token(left out when dead with a refresh token at hand: the provider draws a new one),refresh_token,api_key.connected($name)- a token is kept that is alive or refreshable, or omnipost's options carry one.saveToken()keeps the old refresh token, scopes and account id when the new token lacks them;refresh($name)asks a refreshable provider for a new token and keeps it;disconnect()forgets the token and the account.
Connecting a network
On /admin/social (social_admin_accounts), "connect" (POST, token social-connect-<provider>)
keeps a random state in the session and sends the administrator to the network's consent page
(OAuthInterface::authorizationUrl()). Back at /connect/social/{provider}
(social_oauth_callback), a matching state and a code are exchanged for the token, kept; a
provider with a feed then reads its account. A refused or forged answer is a flash message.
Instagram, for one artist's account: a professional account (creator or business), and a Meta app in development mode with the artist added as an Instagram Tester - no App Review is needed for an account of the app's own.
The feed and the wall
Base\Social\Service\FeedSync::sync(string $provider, ?int $limit = null): int reads the account
(saved) and its last posts (social.feed.limit, 24) through a provider with a feed
(FeedInterface; another is an InvalidConfigException): new ones added, known ones updated
(caption, likes, comments, the network's fresh addresses), the thumbnail - the film's still, or
the picture - copied once into the storage when social.feed.cache_thumbnails (only an answer
of type image/*; a picture that does not answer costs the row its thumbnail and nothing more).
Commands
| Command | Options | What it does |
|---|---|---|
social:sync [provider] |
--limit, -l |
reads the feeds of every provider that has one, or one; an unknown provider is an error; fails (exit 1) when one failed |
social:refresh-tokens |
--days=10, --force, -f |
refreshes the refreshable providers' tokens that die within that many days (Instagram's live sixty and are refreshed only while alive), or every one |
Twig functions
Base\Social\Twig\SocialExtension:
| Function | Renders |
|---|---|
social_wall(limit = 12, provider = null) |
the account's last posts in a grid (@Social/client/_wall.html.twig): the local thumbnail, a film played muted on hover from the network's address while it answers, each opening the post on the network; hidden posts left out |
social_embed(url, consent = false) |
any public post: an Instagram post or reel (Instagram's own embed), a YouTube film (youtube-nocookie). Nothing reaches the network before a click - or at once with consent true, or once a social:consent event is dispatched on document |
social_connected(provider) |
whether the account is connected |
SocialExtension::recognize(string $url) reads an address - instagram.com/<user>/p|reel|reels|tv/<id>
or a YouTube watch, shorts, embed, live or youtu.be address - into {network, permalink, id}:
use Base\Social\Entity\SocialPost;
use Base\Social\Twig\SocialExtension;
print_r(SocialPost::cleanTags(['#violin', ' bach ', 'violin', '']));
print_r(SocialExtension::recognize('https://www.instagram.com/clara/reels/C8xYz12AbC/?igsh=1'));
Array
(
[0] => violin
[1] => bach
)
Array
(
[network] => instagram
[permalink] => https://www.instagram.com/reel/C8xYz12AbC/
[id] => C8xYz12AbC
)
The look is bundles/social/css/social.css, on the host's --social-accent, --social-soft,
--social-surface, --social-radius, --social-gap, --social-columns,
--social-video-ratio, --social-embed-width and --social-embed-video-width. Translations:
the domain social (ICU), in English, French and German.
Propose a change docs/usage-accounts.md
The back office
Loaded when omnibase/admin is installed; the CRUDs keep omnibase/admin's default permissions,
the accounts page is ROLE_ADMIN.
Posts
SocialPostCrudController (slug social-posts): title, kind (reel or picture), state (read),
film, picture, caption, hashtags (typed, split on commas, spaces, semicolons), template (chosen
in a list), use the template, cover's second, hour, link, and the targets (TargetType: network,
enabled, caption, title, hashtags typed as a line, template - "the post's" when empty -,
Instagram's share to feed and YouTube's privacy). A new post gets every network of
social.providers, enabled. Filters: state, template.
Actions on the list, the record and the edit page:
- Preview (
/admin/social-posts/{entityId}/preview) - each network's reel and cover (served by/reel/{target}/video|cover, whatever the state), its checks, its capabilities, its state, permalink and error, whether the account is connected, and a button to send one network again (/retry/{target}). - Render - a
RenderPostMessage; the worker renders. - Publish (asked to confirm) - every enabled network checked first: one refusal and nothing leaves, the reasons listed; all rendered, the publish messages go (at the post's hour), else a rendering followed by the publication.
Templates and the feed
TemplateCrudController (templates): name, default, network (one of social.providers), logo,
its corner and width, band, its text and colours, font, fit, background, fade, intro, outro.
Saving a default takes the flag off the other defaults of the same network.
SocialMediaCrudController (social-medias): the posts the last sync read, read-only but
hidden; Show / hide on the wall and Open the post on the network.
The accounts page
/admin/social, in the admin's layout: per provider, the account connected (name, picture,
followers), when its token dies, how many posts are kept and when the feed was read, and the
buttons - connect, refresh the token, read the feed now, disconnect - and the reading key
(api_key; empty puts the configuration's back). Each is a POST with its token
(social-<action>-<provider>).
The dashboard tile
SocialWidgetType, named social_overview (@Social/admin/widget/overview.html.twig): per
network, the account, the last target that moved and its state, when the feed was read, and a
word when a token without a refresh token dies within ten days.
yield MenuItem::block('social_overview', 'Réseaux', 'fa-solid fa-share-nodes');
Propose a change docs/back-office.md
Configuration reference
social:
Base\Social\DependencyInjection\SocialConfiguration, dumped with Symfony's
YamlReferenceDumper in omnisense/omnibase's harness image:
social:
# The omnipost providers the site uses, by name, in the order the back office lists them.
providers:
# Default:
- instagram
ffmpeg:
# The ffmpeg binary: a name on the PATH, or a path.
binary: ffmpeg
# The ffprobe binary.
ffprobe: ffprobe
# x264 quality: lower is finer and heavier.
crf: 20
# Frames per second of a rendered reel.
fps: 30
# Seconds a rendering may take.
timeout: 900
# The band's font when the template has none.
font: /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
# Where the rendered reels and their covers are kept: <storage>/<post id>/<platform>.mp4.
storage: '%kernel.project_dir%/var/storage/social'
feed:
# Posts read from the account at each sync.
limit: 24
# A local copy of each thumbnail: the networks' own addresses expire.
cache_thumbnails: true
# Seconds the public address of a rendered file still answers once its publication is over.
media_url_ttl: 86400
check:
# Seconds between two looks at a publication the network is still processing.
delay: 30
# Looks before giving up.
attempts: 20
| Option | Type | Default | Read by |
|---|---|---|---|
providers |
list of omnipost provider names | [instagram] |
Accounts, TargetType (only those omnipost declares are used) |
ffmpeg.binary, ffmpeg.ffprobe |
string | ffmpeg, ffprobe |
Renderer (a name on the PATH, or a path) |
ffmpeg.crf |
int, 0 to 51 | 20 |
Renderer |
ffmpeg.fps |
int, 1 to 60 | 30 |
Renderer |
ffmpeg.timeout |
int, 10 or more | 900 |
Renderer, seconds per process |
ffmpeg.font |
path | DejaVu Sans | Renderer (used when the file exists) |
storage |
path | %kernel.project_dir%/var/storage/social |
Renderer |
feed.limit |
int, 1 to 100 | 24 |
FeedSync |
feed.cache_thumbnails |
bool | true |
FeedSync |
media_url_ttl |
int, 60 or more | 86400 |
MediaController |
check.delay |
int, 1 or more | 30 |
Publisher |
check.attempts |
int, 1 or more | 20 |
Publisher |
Parameters: one per key and level (social.providers, social.ffmpeg.binary,
social.check.delay...).
Services
config/services.php autowires and autoconfigures every class of src/ but
DependencyInjection/, Entity/, Enum/, Exception/, Message/, Model/,
Controller/Admin/, Admin/ and the bundle class; Controller/Client/ is tagged
controller.service_arguments; with omnibase/admin, Controller/Admin/ and Admin/. The
three handlers are #[AsMessageHandler], the two commands #[AsCommand]. Accounts needs
omnipost's Registry (its bundle registered) and the core's SettingBagInterface; Publisher
omnipost's Validator.
Propose a change docs/configuration.md
Testing
vendor/bin/phpunit # checked out with its dependencies
php vendor/bin/phpunit -c vendor/omnibase/social/phpunit.xml.dist # from an application
The tests need no kernel. omnisense/omnibase's harness image does not install omnisense/omnipost:
there, its checkouts go on the autoloader with a prepended file that registers the PSR-4
namespaces of every composer.json found under /fam/*/*/:
<?php
// prepend-families.php
$loader = require '/srv/demo/vendor/autoload.php';
foreach (glob('/fam/*/*/composer.json') ?: [] as $file) {
$c = json_decode((string) file_get_contents($file), true) ?: [];
foreach ($c['autoload']['psr-4'] ?? [] as $ns => $path) {
if (!isset($loader->getPrefixesPsr4()[$ns])) {
$loader->addPsr4($ns, dirname($file).'/'.trim((string) $path, './'));
}
}
}
docker run --rm --network none \
-v "$PWD":/srv/demo/vendor/omnibase/social:ro -v ~/Sites/omnipost:/fam/omnipost:ro -v /path/to/tools:/tools:ro \
--entrypoint sh omnisense/omnibase-harness -c \
'cd /srv/demo && APP_ENV=test php -d auto_prepend_file=/tools/prepend-families.php vendor/bin/phpunit -c vendor/omnibase/social/phpunit.xml.dist --do-not-cache-result'
...................... 22 / 22 (100%)
OK (22 tests, 153 assertions)
| File | What |
|---|---|
Service/RendererCommandTest |
a cropped film with logo, band and intro; a contained one with an outro, without sound or logo; a plain one has no concat and maps the dressed streams; the cover command and the helpers |
Service/MediaUrlsTest |
the token round-trips; the address carries the target, the token and the extension |
Entity/SocialPostTest |
toPost() builds the canonical post and a variant per network; for() applies a target's caption, title, hashtags and rendering; an empty override keeps the post's and a picture is an image; the state follows the networks; the file's address closes a day after the end |
Controller/Admin/CrudFormsTest |
the template's enums through EnumType, rendered and translated, named on its detail page; a post takes its targets as objects and chooses its template in a list |
Controller/CrudRequiredFieldsTest |
the CRUDs' required fields |