omnibase/blog is the chronicles of a site of the omnibase family - a
teacher's, a studio's, a wine seller's - and what visitors leave under them or
in the visitors' book, with no account needed. Its namespace is
Base\Blog\; it is built on omnisense/omnibase and omnibase/admin.
What it adds
- A
Postthat is omnisense/omnibase'sThread(a JOINED subclass, tableblog_post): title, headline, excerpt, text (EditorJS or HTML), tags, taxa, owners, slug, states and scheduling, revisions, soft delete and translations come from the thread; the post adds a cover, a "featured" flag, open or closed comments and its reading time. - Comments that are omnisense/omnibase's
Base\Entity\Thread\Comment: under a post, or in the visitors' book when they have no post. - Seven routes: the list (by page, by topic, by taxon), a month, a post, the
RSS feed, the visitors' book, and the two comment forms. The first segment
of the blog's addresses is
blog.path(chroniquesby default). - View models for every page (
Base\Blog\Screen\*): the templates read them, and a client asking for JSON gets the same data. - The comment flow: the guard, the spam classifier, moderation, the administrators notified, the classifier taught by the moderator's choices.
- schema.org's
BlogPostingon a post, the posts in/sitemap.xml. - Twig functions for a host's own pages: the latest posts, the comments, the form, the cards.
- Two back office screens (posts, comments) open to the site's administrator, and a dashboard tile of the comments waiting.
blog:import-wordpress, which moves a WordPress site in through its REST API.- English, French and German messages (domain
blog).
Pages
Proposer une modification docs/index.md
Installation
Requirements
- PHP 8.2 or later;
- doctrine/orm 2.15 or 3;
- omnisense/omnibase 3.0 or 4.0, omnibridge/symfony 1.x, omnibase/admin;
- league/flysystem 3 (the importer's pictures);
- Symfony 6, 7 or 8 (config, console, dependency-injection, form, http-client, http-kernel, security-bundle, string);
- an
App\Entity\Userclass: a signed-in commenter, and the administrators notified.
Suggested: glitchr/ux-google, for Google reCAPTCHA v3 on the comment form when
google.recaptcha.enable is on; omnisense/omnishield, for the captcha of the
forms' guard and the spam classifier's reports.
The bundle
composer require omnibase/blog:dev-main
// config/bundles.php
Base\Blog\BlogBundle::class => ['all' => true],
The routes
# config/routes.yaml
blog_controller:
resource: "@BlogBundle/src/Controller/Client"
type: attribute
prefix: /
The paths start with %blog.path%. After changing blog.path, clear the
route pool: bin/console cache:pool:clear --all (omnisense/omnibase keeps the
routes in a cache pool).
The configuration
Every key is optional (Configuration reference):
# config/packages/blog.yaml
blog:
path: chroniques
sitemap: true
posts_per_page: 12
comments:
enabled: true
guest_allowed: true
guest_replies: false
auto_approve: true
flood_interval: 60
min_delay: 4
max_length: 4000
The schema and the assets
bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate # blog_post (the comments are the core's threadComment)
bin/console assets:install # public/bundles/blog/css/blog.css
A site that had the blog's former comment table moves its rows into
threadComment (Comments).
The layout
The templates extend the site's layout1.html.twig and fill title,
description, content, stylesheets and javascripts: that is the whole
contract. blog.css follows the custom properties --blog-accent,
--blog-ink, --blog-soft, --blog-line, --blog-surface,
--blog-measure and --blog-measure-post.
Overriding the entity
The bundle maps Base\Blog\Entity\* to App\Entity\Blog\* and Base\Blog\Repository\* to App\Repository\Blog\* (omnisense/omnibase's
App-wins aliasing): an App\Entity\Blog\Post extending Base\Blog\Entity\Post
takes over.
Proposer une modification docs/installation.md
Posts
A post is a thread
Base\Blog\Entity\Post extends omnisense/omnibase's Base\Entity\Thread
(table blog_post, discriminator blog_post). From the thread it has:
- its texts, translatable:
title,headline,excerpt,content(EditorJS JSON or HTML); - its
slug, itsowners(the authors), itstags(the topics) andtaxa(what the site classifies with: a level, a subject); - its
state(ThreadState: draft, published...),publishedAt, its revisions, followers, likes and soft delete.
It adds three fields:
| Field | Column | Meaning |
|---|---|---|
cover |
text, nullable | an image (8 MB at most, image/*), stored by omnisense/omnibase's Uploader |
featured |
boolean | the post the first page heads with |
commentsOpen |
boolean, default true | whether visitors comment under it |
Methods
| Method | Returns |
|---|---|
getCover() |
the cover's stored public path (in Twig, post.cover is that path, drawn with ` |
getCoverFile() |
the cover as a file |
hasCover() |
whether a cover is set, without asking the storage |
getCoverUrl() |
the cover's address on the site (/uploads/...); an absolute URL as it is |
getReadingMinutes() |
minutes to read it at 220 words a minute (WORDS_PER_MINUTE), rounded up, one at least; the words of an EditorJS text are read from its blocks |
publish(bool $published = true, ?DateTimeInterface $at = null) |
sets the state to published (or draft) and the publication date to $at, the date it had, or now |
Published
A post is on the site when its state is published and its publication date
is empty or past: a post published with a date to come stays off every list,
page, feed and sitemap until then (PostRepository::published()).
The repository
| Method | Returns |
|---|---|
findPublishedPage($page, $perPage, $tag, $taxon) |
a page of the published posts, newest first, of one tag or one taxon (by slug) when given |
countPublished($tag, $taxon) |
how many |
findAllPublished() |
every published post, for the sitemap |
findLatest($limit = 3) |
the newest posts |
findHeadline() |
the featured post, else the newest |
findOnePublished($slug) |
one published post |
findNeighbours($post) |
the older and the newer post |
findTopics() |
the tags of the posts on the site (published, their publication date come), by priority then slug |
countByMonth() |
year, then month, then the count, for the archive |
findByMonth($year, $month) |
the posts of a month |
Pages are cut by hand (first result, max results), not with Doctrine's paginator, whose output walker does not work with omnisense/omnibase's SQL walker.
JSON-LD
Base\Blog\Service\JsonLd::article($post, $url, $base) returns schema.org's
BlogPosting: headline (the title, cut at 110 characters), url,
mainEntityOfPage, description (the excerpt, else the headline, without
tags), datePublished, dateModified, image (the cover, $base before a
site path), author (a Person per owner), the tags as keywords (joined
by commas) and wordCount, the reading time times 220 (a rounded figure, not
the exact count). The post's page prints it in a
<script type="application/ld+json">.
Proposer une modification docs/concepts.md
The pages
Routes
With blog.path: chroniques:
| Name | Path | Methods | Template | Query |
|---|---|---|---|---|
blog_index |
/chroniques |
any | @Blog/client/index.html.twig |
page, sujet (a tag's slug), classe (a taxon's slug) |
blog_month |
/chroniques/{year}/{month} |
any | @Blog/client/index.html.twig |
|
blog_feed |
/chroniques/feed.xml |
any | @Blog/client/feed.xml.twig |
|
blog_post |
/chroniques/{slug} |
any | @Blog/client/post.html.twig |
|
blog_post_comment |
/chroniques/{slug}/commenter |
POST | ||
blog_book |
/livre-d-or |
any | @Blog/client/book.html.twig |
|
blog_book_comment |
/livre-d-or/commenter |
POST |
year is four digits, month one or two, slug [a-z0-9-]+. The visitors'
book is always at /livre-d-or, whatever blog.path says. An unknown or
unpublished slug is a 404.
What each page shows
- The list: the published posts,
posts_per_pageat a time, newest first. On the first page, without a topic or a taxon, the headline (the featured post, else the newest) is drawn large and left out of the cards. The topics (the tags in use), the months with their counts, the previous and next pages, the feed's address. - A month: its posts, on one page.
- A post: its story (title, cover at 1600 pixels, text, tags, taxa, reading
time), the older and the newer post, its comments and the form when it
takes comments, its
BlogPosting. - The feed: the twenty newest posts, by their absolute addresses, as RSS
(
Content-Type: application/rss+xml; charset=UTF-8), its channel titled with the site's title, else the host. - The visitors' book: the comments without a post, and the form.
View models
Each action hands omnisense/omnibase's ScreenResponder a view: a template
and its model, built by Base\Blog\Screen\BlogScreens.
| Page | Model | Its fields |
|---|---|---|
| list, month | BlogPage |
page, pages, found, next, prev, articles (cards), headline, topics, topic, taxon, month, months, indexUrl, feedUrl |
| post | PostPage |
article (a story), prev, next, discussion, jsonLd |
| book | BookPage |
discussion |
| feed | FeedPage |
title, indexUrl, feedUrl, items |
A Discussion holds the visible comments, whether the visitor may write
(open), whether they may answer (canReply), the sign-in address and the
flashes of the blog category. Covers are drawn at 800 pixels on a card,
1200 for the headline, 1600 on a post (BlogScreens::CARD, HEADLINE,
POST).
The blog's templates read screen and frame alone. A template a site
overrides in templates/bundles/BlogBundle/ also receives the variables the
pages had before the models (posts, post, comments, headline...);
those are deprecated and go in omnisense/omnibase 4.0.
As JSON
A request whose Accept header asks for application/json (and not
text/html) gets the page's model as JSON instead of the HTML, with
Vary: Accept:
curl -H 'Accept: application/json' https://example.org/chroniques
The sitemap
/chroniques is in /sitemap.xml through its route (priority 0.8, daily).
Base\Blog\EventListener\SitemapListener adds every published post on
SitemapEvent::BUILD, dated the day it last changed (its update, else its
publication); blog.sitemap: false leaves the posts out.
Proposer une modification docs/pages.md
Comments
The blog's comments are omnisense/omnibase's Base\Entity\Thread\Comment (table
threadComment): a post is a thread, so a comment under a post has the post as
its thread; a comment with no thread belongs to the visitors' book
(/livre-d-or). The bundle no longer has a Comment entity, enum, form,
repository or guard of its own - it uses the core's:
| Was | Is |
|---|---|
Base\Blog\Entity\Comment (blog_comment, post) |
Base\Entity\Thread\Comment (threadComment, thread) |
Base\Blog\Enum\CommentState |
Base\Enum\CommentState |
Base\Blog\Form\CommentType, Form\Model\CommentModel |
Base\Form\Type\CommentType (options placeholders, trap_class), Base\Form\Model\CommentModel |
Base\Blog\Repository\CommentRepository |
Base\Repository\Thread\CommentRepository |
Base\Blog\Service\CommentGuard |
Base\Service\CommentGuard (the blog passes its min_delay and flood_interval) |
Post::getComments(), getVisibleComments() |
blog_comments(post), blog_comments_count(post) in Twig, or the repository |
The guard is the core's: CommentType's own trap (url) and time (opened),
read by Base\Service\CommentGuard with the blog's min_delay and
flood_interval, and the forms' guard of omnisense/omnibase on top - its stamp,
the lists, the captcha when the site has omnisense/omnishield (printed above the
button). A comment form built by hand in a test posts the guard's signed stamp
(FormGuard::stamp()) and, with omnishield's fixed test gateway,
omnishield-token=omnishield-fixed-token.
What stays the blog's: the routes (blog_book, blog_book_comment,
blog_post_comment), the configuration (blog.comments.*), the moderation
screen (CommentCrudController, /admin/comments, filter thread), the
pending-comments tile (blog_pending_comments), the notifications and the
Akismet reports.
Moving a site
A site that had the blog before: its migration copies blog_comment into
threadComment (post_id becomes thread_id, the discriminator comment) and
drops blog_comment. Apfelschule's migrations/Version20261004090000.php is the
model. In the site's code: Base\Blog\Entity\Comment → Base\Entity\Thread\Comment,
Base\Blog\Enum\CommentState → Base\Enum\CommentState.
Who may write
Base\Blog\Service\CommentPolicy reads blog.comments.*:
| Question | Answer |
|---|---|
| may one comment here? | comments are enabled, the visitor is signed in or guest_allowed is on, and the post keeps its comments open (Post::isCommentsOpen()); the visitors' book has no post to close |
| may one answer a comment? | the visitor is signed in, or guest_replies is on |
A post with closed comments refuses a comment sent anyway with a 403; closed
comments everywhere (or no account when one is needed) refuse it the same
way. A reply sent by a visitor who may not reply is refused on the form
(@blog.comment.error.reply_signed_in), and a reply to a comment that is not
visible, or not under the same post, is kept as a comment of its own.
What happens to a comment
CommentController (blog_post_comment, blog_book_comment) handles the
form in this order:
- the guard (
Base\Service\CommentGuard, withmin_delayandflood_interval): a robot caught in the trap is thanked as if its comment waited (comment.flash.pending) and nothing is kept; a comment sent too fast or too soon after the last one from the same address gets the error@blog.comment.error.<reason>; - the form's validation, with omnisense/omnibase's spam extension: the
comment is classified (Akismet with the site's key,
base.spam.akismet), a blatant spam refused, a doubtful one kept as pending; - the comment is saved with the visitor's address and browser, online at
once when
auto_approveis on and the classifier found it clean, pending otherwise; - the administrators are told (Notifications);
- the visitor goes back to the page: at
#commentaire-<id>withcomment.flash.published, or at#commentaireswithcomment.flash.pending.
An invalid form shows the post (or the book) again with its errors, status
422. The flashes are in the blog category; the page's view model reads
them.
Notifications
Base\Blog\Service\CommentNotifier sends every user whose roles reach
ROLE_ADMIN a notification in omnisense/omnibase's notification centre (the
notify channel, the bell): notify.pending or notify.approved (domain
blog, with the visitor's name and the post's title, or the book's), the
first 240 characters of the comment, and a link to the comments screen
filtered on the comment's state. No e-mail is sent.
Teaching the classifier
Base\Blog\Service\AkismetReporter tells the classifier when a moderator
corrects it: approving a comment held as spam, or one with a spam score,
reports it as ham; marking as spam a comment that was online, or one with no
score, reports it as spam. It sends what the visitor sent (text, author,
address, browser, website). Without omnisense/omnishield nothing is sent; a
provider's error is logged, not shown to the moderator.
Proposer une modification docs/comments.md
In a host's pages
Twig functions
| Function | Returns |
|---|---|
blog_recent(limit = 3) |
the newest published posts |
blog_comments(post = null) |
the visible comments of a post, or of the visitors' book |
blog_comments_count(post = null) |
how many |
blog_comments_open(post = null) |
whether the visitor may comment there |
blog_can_reply() |
whether the visitor may answer a comment |
blog_comment_form(post = null) |
the comment form's view, posting to the post or to the book |
blog_card(post, width = 800) |
a post's card, the model _card.html.twig reads |
blog_discussion(post = null, comments = null) |
the discussion model _comments.html.twig reads; the comments given, else the visible ones |
The partials
| Partial | Variables |
|---|---|
@Blog/client/_card.html.twig |
card (a Base\Screen\Card), or a host's own post entity, made a card with blog_card() |
@Blog/client/_meta.html.twig |
the same: the date, the reading time, the taxa and tags |
@Blog/client/_comments.html.twig |
discussion, or a host's post (null: the visitors' book) and comments, made a discussion with blog_discussion(); comment_form, else the blog's form when the discussion is open |
@Blog/client/_comment.html.twig |
one comment |
A home page with the three latest posts and the visitors' book:
{% for post in blog_recent(3) %}
{% include '@Blog/client/_card.html.twig' with {post: post} %}
{% endfor %}
{% include '@Blog/client/_comments.html.twig' with {post: null} %}
Overriding
A site overrides a template in templates/bundles/BlogBundle/ (for instance
templates/bundles/BlogBundle/client/post.html.twig). Such a template gets
the model (screen) and, until omnisense/omnibase 4.0, the former variables
(The pages).
The stylesheet
bundles/blog/css/blog.css is added by every page's stylesheets block. Its
custom properties: --blog-accent, --blog-ink, --blog-soft,
--blog-line, --blog-surface, --blog-measure (the page's width, 72rem
by default) and --blog-measure-post (a post's text, 46rem).
Translations
The domain blog, in English, French and German (ICU format): index,
post, book, comment (the form, its errors, the flashes), notify,
admin. A site overrides a message in its own
translations/blog+intl-icu.<locale>.yaml.
Proposer une modification docs/templates.md
Moving a WordPress site in: blog:import-wordpress
bin/console blog:import-wordpress https://example.org
bin/console blog:import-wordpress https://example.org --map about-me/biography=bio --map contact=skip --update
bin/console blog:import-wordpress https://example.org --map var/wordpress-map.json --author=admin
bin/console blog:import-wordpress https://example.org --targets # the targets this site has
It reads the site's REST API (wp-json/wp/v2, open on any public WordPress:
no key) - every page of posts, pages, media, categories, tags -
and for each post and page:
- cleans its HTML: the scripts and styles plugins add (share buttons,
forms that need their JavaScript), unexpanded shortcodes, empty
paragraphs, the theme's classes, inline sizes and
srcset; - copies its pictures into the uploads (
local.uploads, under--uploads,wordpress/by default:/uploads/wordpress/2014/05/book-a.jpg) - what is there already is left;--no-medialeaves them on the old site; - rewrites its links: a picture to its copy (a thumbnail
-300x225to the full picture), a post or a page to its new address - by permalink, by?p=123, by?page_id=123, a dated permalink by its slug -, the old home page to/; a page that was not imported keeps its old address; - sends it to its target.
Targets and the map
| Target | Of omnibase/blog | |
|---|---|---|
post |
yes | an omnibase/blog Post under the same slug: title, excerpt, text, its featured picture as its cover, published at its date (a draft stays one), its categories and tags as tags |
skip |
yes | left behind |
| anything else | the site's | a class implementing Base\Blog\WordPress\TargetInterface |
By default a post goes to post, a page to page when the site declares a
target of that name, else to skip. The map says otherwise, for one item - by
its slug or its path (about-me/biography) - or for all of a kind (*post,
*page): --map slug=target, repeatable, or a JSON file
({"about-me/biography": "bio", "schedule-2016": "agenda:2016", "*page": "page"}).
What follows the colon is handed to the target ($context->option($item)).
namespace App\Import;
use Base\Blog\WordPress\{Context, Item, Result, TargetInterface};
final class BiographyTarget implements TargetInterface // autoconfigured: tag blog.wordpress_target
{
public function getName(): string { return 'bio'; }
public function url(Item $item): ?string { return '/biography'; } // before anything is written: the links follow
public function import(Item $item, Context $context): string
{
$bio = $this->bios->findOneFor(...);
if ($bio && !$context->update) {
return Result::KEPT;
}
// $item->title, ->content (cleaned, links rewritten), ->text(), ->excerpt, ->date, ->categories...
$this->em->persist($bio); // persist; the importer flushes
return $bio->getId() ? Result::UPDATED : Result::CREATED;
}
}
The cover
A post's featured picture (featured_media) becomes its cover: the importer
gives every item the address of that picture's copy ($item->cover,
/uploads/wordpress/2014/05/a.jpg; $item->featuredMedia is its id on the
old site), and the post target uploads it as Post::$cover - once: a cover
set since, by hand or by an earlier run, is kept. With --no-media there is
no copy, so no cover. A site's own target reads $item->cover the same way.
A slug already taken
omnibase keeps a thread's slug unique whatever its kind: a post named like a
product or a page cannot have that slug. The post target then takes the
slug with the post's number on the old site (trophees-publicitaires-2170),
the same at every run, so the post is found again and never doubled.
Run it again
Without --update, what an earlier run imported is left alone (kept);
with it, brought up to date. Either way nothing is doubled: a target finds
its record again (a post by its slug). The summary counts, per target, what
was created, updated, kept and skipped, and the pictures copied.
In code
$result = $importer->import('https://example.org', ['contact' => 'skip'], update: true);
$items = $importer->read('https://example.org'); // the posts and pages, cleaned, not imported
Base\Blog\WordPress\Client reads the API alone (all($site, 'pages')).
Proposer une modification docs/wordpress-import.md
The back office
The screens are loaded when omnibase/admin is installed. Both carry
omnibase/admin's #[OpenToAdmins]: the site's administrator (ROLE_ADMIN: a
teacher, an editor) writes in them, not the super-admin only.
Posts
Base\Blog\Controller\Admin\Crud\PostCrudController creates, edits and
deletes a post: title, state, slug (following the title), publication date,
featured, comments open, headline, excerpt (with a help text), content (the
editor, with collaborative autosave and live editing), tags, taxa and cover.
A post published with a date to come waits for it (Posts).
Comments
Base\Blog\Controller\Admin\Crud\CommentCrudController moderates the
comments (omnisense/omnibase's Base\Entity\Thread\Comment, every thread's).
There is no "new" action: comments come from the visitors. Fields: state,
name, e-mail, website, post (thread), parent, content, date; the address and
the spam score on the detail page. Filters: state, post.
Three actions, on the index and on the detail page, open to the
administrator (#[OpenToAdmins(actions: ['approve', 'spam', 'trash'])]):
| Action | Path under the screen | Does | The classifier is told |
|---|---|---|---|
approve |
/{entityId}/approve |
its state becomes approved: online | ham, when it was held as spam or had a spam score |
spam |
/{entityId}/spam |
its state becomes spam | spam, when it was online or had no score |
trash |
/{entityId}/trash |
its state becomes trash | nothing |
Each flashes @blog.admin.comment.flash.approved, .spam or .trashed and
returns to the index. The notifications of a new comment link to this screen,
filtered on the comment's state (Comments).
The dashboard tile
Base\Blog\Admin\Widget\PendingCommentsWidgetType is the widget
blog_pending_comments: how many comments wait (countPending()) and the
five latest (findPending(5)), rendered by
@Blog/admin/widget/pending_comments.html.twig.
yield MenuItem::block('blog_pending_comments', 'Commentaires en attente', 'fa-solid fa-comments')->setSize(2);
yield MenuItem::linkToCrud(\Base\Blog\Entity\Post::class, 'Chroniques', 'fa-solid fa-newspaper');
yield MenuItem::linkToCrud(\Base\Entity\Thread\Comment::class, 'Commentaires', 'fa-solid fa-comments');
Proposer une modification docs/back-office.md
Configuration reference
The blog tree
Dumped with Symfony's YamlReferenceDumper from
Base\Blog\DependencyInjection\BlogConfiguration:
blog:
# The first segment of the blog's addresses: /chroniques, /chroniques/{slug}, /chroniques/feed.xml. No slash.
path: chroniques
# The published posts in /sitemap.xml.
sitemap: true
# Posts listed per page.
posts_per_page: 12
comments:
# Comments at all.
enabled: true
# Comments without an account (a name and an e-mail).
guest_allowed: true
# Replies without an account. Off: anyone writes a word, answering one takes an account.
guest_replies: false
# A comment Akismet finds clean goes online at once; otherwise every comment waits.
auto_approve: true
# Seconds between two comments from the same address.
flood_interval: 60
# Seconds between the form being opened and sent: faster is a robot.
min_delay: 4
max_length: 4000
| Key | Type | Default | Read by |
|---|---|---|---|
path |
string | chroniques |
the routes' first segment; one or more segments, no slash at either end (/blog is refused) |
sitemap |
boolean | true | SitemapListener |
posts_per_page |
integer, at least 1 | 12 | the list |
comments.enabled |
boolean | true | CommentPolicy |
comments.guest_allowed |
boolean | true | CommentPolicy: without it, commenting takes an account |
comments.guest_replies |
boolean | false | CommentPolicy: with it, answering a comment takes no account |
comments.auto_approve |
boolean | true | a comment the classifier finds clean goes online at once; off, every comment waits |
comments.flood_interval |
integer, at least 0 | 60 | the guard: seconds between two comments from one address |
comments.min_delay |
integer, at least 0 | 4 | the guard: seconds between the form's opening and its sending |
comments.max_length |
integer, at least 100 | 4000 | the comment form's max_length |
Each key is a container parameter: %blog.path%, %blog.sitemap%,
%blog.posts_per_page%, %blog.comments.enabled%...
%blog.comments.max_length%.
Services
config/services.php autowires and autoconfigures the bundle's classes; the
admin screens and the widget are loaded with omnibase/admin.
| Service | Role |
|---|---|
Base\Blog\Repository\PostRepository |
the post queries (Posts) |
Base\Blog\Screen\BlogScreens |
the pages' view models, card(), discussion() |
Base\Blog\Service\CommentPolicy |
isOpen($post, $signedIn), canReply($signedIn) |
Base\Blog\Service\CommentNotifier |
commentLeft($comment) |
Base\Blog\Service\AkismetReporter |
reportSpam($comment), reportHam($comment) |
Base\Blog\Service\JsonLd |
article($post, $url, $base) |
Base\Blog\EventListener\SitemapListener |
the posts of /sitemap.xml |
Base\Blog\Twig\BlogExtension |
the Twig functions |
Base\Blog\Command\ImportWordPressCommand |
blog:import-wordpress |
A service tagged blog.wordpress_target (Base\Blog\WordPress\TargetInterface,
autoconfigured) is a target of the WordPress import
(Moving a WordPress site in).
Proposer une modification docs/configuration.md
Testing
Running the suite
From the package's checkout, with its dev dependencies:
vendor/bin/phpunit
Inside a host application:
php vendor/bin/phpunit -c vendor/omnibase/blog/phpunit.xml.dist
In omnisense/omnibase-harness, mounting the checkout in place of the installed package:
docker run --rm --network none \
-v "$PWD":/srv/demo/vendor/omnibase/blog:ro \
omnisense/omnibase-harness:latest test blog
On this checkout, in the harness:
Tests: 31, Assertions: 177, Failures: 1, Skipped: 1.
The skipped test is the captcha's: the harness asks the captcha only after
refused tries (base.guard.captcha_after). The failure is the snapshot test
(below).
What the tests check
| Test | Checks |
|---|---|
Controller\Admin\OpenToAdminsTest |
the administrator writes in both screens; anyone else is refused (needs a host kernel) |
Controller\CommentGuardTest |
a comment written as a person goes through; a filled trap is a robot; a comment sent faster than min_delay is refused; a comment without the captcha's token is refused, when the host has a captcha |
Controller\CrudRequiredFieldsTest |
omnibase/admin's CrudFieldsTestCase on the two screens |
DependencyInjection\ConfigurationTest |
a site that says nothing keeps chroniques; a site's own path; a path with slashes around is refused |
Screen\BlogPagesSnapshotTest |
the pages equal to the recorded HTML; the screens given as data |
Service\AkismetReporterTest |
a spam let through is reported as the visitor sent it; without a classifier nothing is sent |
Service\JsonLdTest |
a post is a BlogPosting; a post without cover nor author keeps to what it knows; a long title is cut; the cover's address |
WordPress\ImporterTest |
on a recorded excerpt of a real site's API: the client follows every page, the HTML is cleaned, the links follow what was imported, a second run doubles nothing, no media means no cover, an unknown target is refused |
The snapshots
Screen\BlogPagesSnapshotTest renders the pages through the host's kernel
(the list and its variants, a post seen by a visitor and by a member, the
feed, the book, a refused comment shown again with status 422, the partials
a host page includes) and compares them with tests/Screen/snapshots/*.html.
Every date is fixed; the CSRF token, the form's opening time and the guard's
stamp are set aside. A missing snapshot is written and the test marked
incomplete; BLOG_SNAPSHOT_RECORD=1 writes them all again.
The host's built core assets (/bundles/base/<entry>.<hash>.css|js: the base-async,
base-defer, form-defer entry points) are set aside on both sides: they are printed only
where the host built them, and their hash changes at each build.