Read from the package's own PHP files at the commit b9185af96c80: its namespaces, classes, interfaces and enums, their public constants, properties and methods with their PHPDoc, and the options its factories take. Nothing of it is run.
NewsletterBundleclassA newsletter with no account behind it: addresses that confirmed (Subscriber), letters written in the back office and sent through Messenger (Campaign), a monthly digest drafted from what the site has to announce (Digest\DigestSourceInterface).
3 methods
SubscribersWidgetTypeclassThe dashboard's tile: how many read the letters, how many have not confirmed yet, the last few to sign up, each opening the list.
4 methods
DraftDigestCommandclassThe monthly digest, drafted for the coming month (a cron on the 20th, say): a DRAFT campaign in the back office, to read and send. Nothing is sent.
1 method
Base\Newsletter\Controller\Admin\Crud
CampaignCrudControllerclassWriting a letter: a subject, a text, maybe a button, who it goes to (one language, or everyone). Two buttons on a draft: "send me a test" - to the signed-in administrator's own address - and "send", which asks first and hands the letter to Messenger; the list sees SENDING, then SENT and how many it went to.
8 methods
SubscriberCrudControllerclassThe list: who signed up, from where, who confirmed, who left. Read more than written - an address is added by itself confirming -, and the confirmed ones exported as a CSV for whatever the site moves to next.
7 methods
Base\Newsletter\Controller\Client
NewsletterControllerclassSigning up, confirming, leaving. Double opt-in: the address confirms from its mail before anything else is sent to it. The answer to a sign-up never says whether the address was already on the list - the same words for a new one, a pending one, a confirmed one -, so the form cannot be used to find out who reads the letters.
6 methods
Base\Newsletter\DependencyInjection
NewsletterConfigurationclass1 method
NewsletterExtensionclass3 methods · 1 constant
DigestSourceInterfaceinterfaceSomething the site announces in the monthly digest: the concerts of an agenda, the posts of a blog, the new recordings. A service implementing it is found by itself (tag newsletter.digest_source) and asked, for the coming month, what it has.
2 methods
CampaignclassA letter: a subject, a text (plain, a blank line between two paragraphs), maybe a button to a page, and who it goes to - the subscribers of one language, or everyone. Written in the back office, tried on oneself, then sent: SENDING while the mails go out, SENT with the number they went to.
22 methods
SubscriberclassAn address on the list - never an account. Double opt-in: it receives nothing before it confirms from the mail it was sent (confirmedAt). Going away keeps the row (unsubscribedAt): the history is known, nothing is sent to it any more, and coming back is a new confirmation, under a new token - the links of the old mails no longer answer.
23 methods
CampaignStateenumWhere a letter is: being written, on its way (the message dispatched, the mails going out), sent. A string enum (not omnibase's EnumType) so the column reads plainly in the database and in the admin's filters.
3 cases
SubscribeTypeclassThe sign-up form - one address, and `source`: which page it was filled on.
4 methods
SendCampaignMessageclass"Send this letter": dispatched by the back office's send button, handled by MessageHandler\SendCampaignHandler. Routed to an async transport by the host (messenger.yaml: Base\Newsletter\Message\SendCampaignMessage: async), so the button answers at once whatever the list's length.
1 method · 1 property
Base\Newsletter\MessageHandler
SendCampaignHandlerclassA letter sent: one mail per confirmed subscriber - of its language when it has one -, each remembered on the subscriber as it goes, so a send that fails half-way and is retried does not write twice to the first half. An address its transport refuses is skipped and logged, not fatal. Then the campaign is SENT, with the number of mails.
2 methods
CampaignRepositoryclass1 method
SubscriberRepositoryclass8 methods
DigestDrafterclassThe monthly digest, drafted: every source asked what it has between two dates, each a part of the letter under its name, its entries in date order - a title with its date, a line, a link. A Campaign in DRAFT comes out, to be read, touched up and sent from the back office; nothing is saved here (Command\DraftDigestCommand persists it), nothing is sent.
3 methods
NewsletterMailerclassThe newsletter's two mails.
3 methods
SignedTimestampclassThe time the form was opened, signed with the kernel's secret: a robot cannot send an old timestamp of its own making to look patient.
2 methods
SubscribeGuardclassWhat stops someone using the form to flood a stranger's inbox with confirmation mails: a second sign-up from the same address within the flood interval, more than the limiter allows in the hour. The robots are the form's guard's (glitchr/omnibase's option `guard`); with a core from before it, the form's own trap and signed time are read here: the trap filled (thanked and dropped, so the robot learns nothing), a form sent faster than a person types or with a time it did not get from us. Each answers with a reason the controller turns into a flash.
2 methods · 4 constants
NewsletterExtensionclassWhat a host's own pages ask the newsletter: the sign-up form for a footer or a home page (its source: the page's key, kept with the address), and how many read the letters.
2 methods
Base\Newsletter
NewsletterBundle class
class NewsletterBundle extends AbstractBaseBundle NewsletterBundle.php:15
A newsletter with no account behind it: addresses that confirmed (Subscriber), letters written in the back office and sent through Messenger (Campaign), a monthly digest drafted from what the site has to announce (Digest\DigestSourceInterface).
NewsletterBundle::__construct()
public function __construct()NewsletterBundle::getPath()
public function getPath(): stringModern layout: the class lives in src/, the bundle root is the package root.
NewsletterBundle::build()
public function build(ContainerBuilder $container): voidBase\Newsletter\Admin\Widget
SubscribersWidgetType class
final class SubscribersWidgetType implements DashboardWidgetTypeInterface SubscribersWidgetType.php:15
The dashboard's tile: how many read the letters, how many have not confirmed yet, the last few to sign up, each opening the list.
yield MenuItem::block('newsletter_subscribers', ...) in the dashboard's
configureWidgetItems() places it.
SubscribersWidgetType::__construct()
public function __construct(readonly SubscriberRepository $subscribers)SubscribersWidgetType::getName()
public static function getName(): stringSubscribersWidgetType::getTemplate()
public function getTemplate(): stringSubscribersWidgetType::getTemplateVars()
public function getTemplateVars(MenuItem $widget): arrayBase\Newsletter\Command
DraftDigestCommand class
final class DraftDigestCommand extends Command DraftDigestCommand.php:22
The monthly digest, drafted for the coming month (a cron on the 20th, say): a DRAFT campaign in the back office, to read and send. Nothing is sent.
bin/console newsletter:draft-digest bin/console newsletter:draft-digest --from=2026-11-01 --to=2026-12-01 --locale=de
DraftDigestCommand::__construct()
public function __construct(readonly DigestDrafter $drafter, readonly EntityManagerInterface $entityManager, readonly bool $enabled = true, readonly string $defaultLocale = 'en')Base\Newsletter\Controller\Admin\Crud
CampaignCrudController class
class CampaignCrudController extends AbstractCrudController CampaignCrudController.php:34
Writing a letter: a subject, a text, maybe a button, who it goes to (one language, or everyone). Two buttons on a draft: "send me a test" - to the signed-in administrator's own address - and "send", which asks first and hands the letter to Messenger; the list sees SENDING, then SENT and how many it went to.
CampaignCrudController::setNewsletterServices()
public function setNewsletterServices(NewsletterMailer $mailer, MessageBusInterface $bus, SubscriberRepository $subscribers, TranslatorInterface $translator, array $locales = []): voidCampaignCrudController::getEntityFqcn()
public static function getEntityFqcn(): stringCampaignCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringCampaignCrudController::configureFilters()
public function configureFilters(Filters $filters): FiltersCampaignCrudController::configureFields()
public function configureFields(string $pageName): iterableCampaignCrudController::configureActions()
public function configureActions(Actions $actions): ActionsCampaignCrudController::test()
public function test(string $entityId): ResponseThe letter as the subscribers will get it, to the signed-in administrator.
CampaignCrudController::send()
public function send(string $entityId): ResponseSubscriberCrudController class
class SubscriberCrudController extends AbstractCrudController SubscriberCrudController.php:26
The list: who signed up, from where, who confirmed, who left. Read more than written - an address is added by itself confirming -, and the confirmed ones exported as a CSV for whatever the site moves to next.
SubscriberCrudController::setNewsletterServices()
public function setNewsletterServices(SubscriberRepository $subscribers): voidSubscriberCrudController::getEntityFqcn()
public static function getEntityFqcn(): stringSubscriberCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringSubscriberCrudController::configureFilters()
public function configureFilters(Filters $filters): FiltersSubscriberCrudController::configureFields()
public function configureFields(string $pageName): iterableSubscriberCrudController::configureActions()
public function configureActions(Actions $actions): ActionsSubscriberCrudController::export()
public function export(): ResponseThe confirmed addresses still on the list, as a CSV: e-mail, language, since, source.
Base\Newsletter\Controller\Client
NewsletterController class
class NewsletterController extends AbstractController NewsletterController.php:26
Signing up, confirming, leaving. Double opt-in: the address confirms from its mail before anything else is sent to it. The answer to a sign-up never says whether the address was already on the list - the same words for a new one, a pending one, a confirmed one -, so the form cannot be used to find out who reads the letters.
NewsletterController::__construct()
public function __construct(readonly SubscriberRepository $subscribers, readonly EntityManagerInterface $entityManager, readonly SubscribeGuard $guard, readonly NewsletterMailer $mailer, readonly bool $doubleOptIn = true)NewsletterController::index()
public function index(): ResponseA page of its own: what the letter is, how often, the form.
NewsletterController::subscribe()
public function subscribe(Request $request): ResponseNewsletterController::confirm()
public function confirm(string $token): ResponseNewsletterController::unsubscribe()
public function unsubscribe(Request $request, string $token): ResponseGET from the mail's link: a page with one button (mail scanners follow links, they must not unsubscribe anyone). POST from that button, or from a mail client's one-click List-Unsubscribe (RFC 8058) - which sends no CSRF token: the token of the address is the secret. The row stays, marked; an unknown token is answered "done" all the same.
NewsletterController::form()
public function form(string $source = 'site'): FormInterfaceThe form a page embeds (the Twig function newsletter_form(), @Newsletter/client/_form.html.twig).
Base\Newsletter\DependencyInjection
NewsletterConfiguration class
class NewsletterConfiguration extends AbstractBaseConfiguration NewsletterConfiguration.php:8
NewsletterConfiguration::getConfigTreeBuilder()
public function getConfigTreeBuilder(): TreeBuilderNewsletterExtension class
class NewsletterExtension extends AbstractBaseExtension implements PrependExtensionInterface NewsletterExtension.php:16
The limiter of the sign-up form: framework.rate_limiter.newsletter_subscribe.
NewsletterExtension::getConfiguration()
public function getConfiguration(array $config, ContainerBuilder $container): NewsletterConfigurationNewsletterExtension::prepend()
public function prepend(ContainerBuilder $container): voidA few sign-ups an hour per visitor, so the form cannot be used to flood someone's inbox with confirmation mails. Declared first: a host's own framework.rate_limiter.newsletter_subscribe, read after, wins.
NewsletterExtension::load()
public function load(array $configs, ContainerBuilder $container): voidBase\Newsletter\Digest
DigestSourceInterface interface
interface DigestSourceInterface DigestSourceInterface.php:13
Something the site announces in the monthly digest: the concerts of an agenda, the posts of a blog, the new recordings. A service implementing it is found by itself (tag newsletter.digest_source) and asked, for the coming month, what it has.
DigestSourceInterface::getName()
public function getName(): stringThe heading of its part of the letter: "Concerts", "On the blog".
DigestSourceInterface::digest()
public function digest(\DateTimeImmutable $from, \DateTimeImmutable $to, string $locale): arrayWhat happens (or was published) between $from and $to, in $locale.
Base\Newsletter\Entity
Campaign class
class Campaign Campaign.php:16
A letter: a subject, a text (plain, a blank line between two paragraphs), maybe a button to a page, and who it goes to - the subscribers of one language, or everyone. Written in the back office, tried on oneself, then sent: SENDING while the mails go out, SENT with the number they went to.
Campaign::__construct()
public function __construct(?string $subject = null, ?string $body = null, ?string $locale = null)Campaign::__toString()
public function __toString(): stringCampaign::getId()
public function getId(): ?intCampaign::getSubject()
public function getSubject(): ?stringCampaign::setSubject()
public function setSubject(?string $subject): selfCampaign::getBody()
public function getBody(): ?stringCampaign::setBody()
public function setBody(?string $body): selfCampaign::getLink()
public function getLink(): ?stringCampaign::setLink()
public function setLink(?string $link): selfCampaign::getLinkLabel()
public function getLinkLabel(): ?stringCampaign::setLinkLabel()
public function setLinkLabel(?string $linkLabel): selfCampaign::getLocale()
public function getLocale(): ?stringCampaign::setLocale()
public function setLocale(?string $locale): selfCampaign::getState()
public function getState(): CampaignStateCampaign::isDraft()
public function isDraft(): boolCampaign::isSent()
public function isSent(): boolCampaign::getCreatedAt()
public function getCreatedAt(): \DateTimeImmutableCampaign::getSentAt()
public function getSentAt(): ?\DateTimeImmutableCampaign::getRecipients()
public function getRecipients(): intCampaign::markSending()
public function markSending(): selfOn its way: the message is dispatched. Only a draft goes.
Campaign::markSent()
public function markSent(int $recipients): selfCampaign::getParagraphs()
public function getParagraphs(): array- Returns
the paragraphs: what a blank line separates
Subscriber class
class Subscriber Subscriber.php:18
An address on the list - never an account. Double opt-in: it receives nothing before it confirms from the mail it was sent (confirmedAt). Going away keeps the row (unsubscribedAt): the history is known, nothing is sent to it any more, and coming back is a new confirmation, under a new token - the links of the old mails no longer answer.
The token is the key of the confirm and unsubscribe links: 32 hex characters, as hard to guess as a password and never shown elsewhere.
Subscriber::__construct()
public function __construct(string $email, string $locale = 'en', string $source = 'site', ?string $ip = null)Subscriber::__toString()
public function __toString(): stringSubscriber::normalize()
public static function normalize(string $email): stringThe way an address is kept and looked up: trimmed, lowercased.
Subscriber::getId()
public function getId(): ?intSubscriber::getEmail()
public function getEmail(): stringSubscriber::getToken()
public function getToken(): stringSubscriber::getLocale()
public function getLocale(): stringSubscriber::setLocale()
public function setLocale(string $locale): selfSubscriber::getCreatedAt()
public function getCreatedAt(): \DateTimeImmutableSubscriber::getConfirmedAt()
public function getConfirmedAt(): ?\DateTimeImmutableSubscriber::getUnsubscribedAt()
public function getUnsubscribedAt(): ?\DateTimeImmutableSubscriber::getSource()
public function getSource(): stringSubscriber::getIp()
public function getIp(): ?stringSubscriber::isConfirmed()
public function isConfirmed(): boolIt confirmed - whether or not it left since.
Subscriber::isUnsubscribed()
public function isUnsubscribed(): boolSubscriber::isActive()
public function isActive(): boolWhat the letters go to: confirmed, and still there.
Subscriber::isPending()
public function isPending(): boolSigned up, not confirmed yet.
Subscriber::confirm()
public function confirm(): selfThe link of its mail was followed. Once is enough (the first date is kept); an address that left is not brought back by an old link.
Subscriber::unsubscribe()
public function unsubscribe(): selfSubscriber::resubscribe()
public function resubscribe(bool $doubleOptIn = true): selfBack on the list after leaving. With double opt-in it confirms again, under a new token; without, it is on the list at once. An address still on the list is left as it is.
Subscriber::getLastCampaign()
public function getLastCampaign(): ?intSubscriber::wasSent()
public function wasSent(Campaign $campaign): boolSubscriber::sent()
public function sent(Campaign $campaign): selfBase\Newsletter\Enum
CampaignState enum
enum CampaignState CampaignState.php:10
Where a letter is: being written, on its way (the message dispatched, the mails going out), sent. A string enum (not omnibase's EnumType) so the column reads plainly in the database and in the admin's filters.
Base\Newsletter\Form
SubscribeType class
final class SubscribeType extends AbstractType SubscribeType.php:25
The sign-up form - one address, and source: which page it was filled on.
Guarded as glitchr/omnibase guards a form (its option guard, action
"newsletter", newsletter.min_delay): a trap, the time it takes, the lists,
the captcha when the site has glitchr/omnishield. With a glitchr/omnibase
from before the guard, the form's own: a trap field (website) and the
time it was opened (opened_at, signed with the kernel's secret), read by
Service\SubscribeGuard - until the host takes a core that has the guard.
SubscribeType::__construct()
public function __construct(readonly string $secret, readonly int $minDelay = 3)SubscribeType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidSubscribeType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidSubscribeType::getBlockPrefix()
public function getBlockPrefix(): stringBase\Newsletter\Message
SendCampaignMessage class
final readonly class SendCampaignMessage SendCampaignMessage.php:11
"Send this letter": dispatched by the back office's send button, handled by MessageHandler\SendCampaignHandler. Routed to an async transport by the host (messenger.yaml: Base\Newsletter\Message\SendCampaignMessage: async), so the button answers at once whatever the list's length.
SendCampaignMessage::__construct()
public function __construct(public int $campaignId)Base\Newsletter\MessageHandler
SendCampaignHandler class
final class SendCampaignHandler SendCampaignHandler.php:23
A letter sent: one mail per confirmed subscriber - of its language when it has one -, each remembered on the subscriber as it goes, so a send that fails half-way and is retried does not write twice to the first half. An address its transport refuses is skipped and logged, not fatal. Then the campaign is SENT, with the number of mails.
SendCampaignHandler::__construct()
public function __construct(readonly CampaignRepository $campaigns, readonly SubscriberRepository $subscribers, readonly NewsletterMailer $mailer, readonly EntityManagerInterface $entityManager, readonly LoggerInterface $logger = new NullLogger())SendCampaignHandler::__invoke()
public function __invoke(SendCampaignMessage $message): voidBase\Newsletter\Repository
CampaignRepository class
class CampaignRepository extends ServiceEntityRepository CampaignRepository.php:10
CampaignRepository::__construct()
public function __construct(ManagerRegistry $registry)SubscriberRepository class
class SubscriberRepository extends ServiceEntityRepository SubscriberRepository.php:11
SubscriberRepository::__construct()
public function __construct(ManagerRegistry $registry)SubscriberRepository::findOneByEmail()
public function findOneByEmail(string $email): ?SubscriberSubscriberRepository::findOneByToken()
public function findOneByToken(string $token): ?SubscriberSubscriberRepository::findConfirmed()
public function findConfirmed(?string $locale = null): arrayWhat the letters go to: confirmed and still there - of one language when one is given.
SubscriberRepository::countConfirmed()
public function countConfirmed(?string $locale = null): intSubscriberRepository::countPending()
public function countPending(): intSigned up, not confirmed yet, not gone.
SubscriberRepository::findLatest()
public function findLatest(int $limit = 5): array- Returns
the last ones signed up, whatever became of them
SubscriberRepository::findLastFromIp()
public function findLastFromIp(string $ip): ?SubscriberThe last sign-up from this address, to hold the flood interval.
Base\Newsletter\Service
DigestDrafter class
final class DigestDrafter DigestDrafter.php:19
The monthly digest, drafted: every source asked what it has between two dates, each a part of the letter under its name, its entries in date order - a title with its date, a line, a link. A Campaign in DRAFT comes out, to be read, touched up and sent from the back office; nothing is saved here (Command\DraftDigestCommand persists it), nothing is sent.
No source with anything to say: no draft.
DigestDrafter::__construct()
public function __construct(readonly iterable $sources = [], readonly ?TranslatorInterface $translator = null, readonly ?string $title = null)DigestDrafter::nextMonth()
public static function nextMonth(?\DateTimeImmutable $today = null): arrayThe coming month: from its first day to the first of the next (exclusive).
@return array{0: \DateTimeImmutable, 1: \DateTimeImmutable}
DigestDrafter::draft()
public function draft(\DateTimeImmutable $from, \DateTimeImmutable $to, ?string $locale = null, string $fallbackLocale = 'en'): ?Campaign$localethe language of the letter and of who receives it; null: everyone, in $fallbackLocale
NewsletterMailer class
final class NewsletterMailer NewsletterMailer.php:34
The newsletter's two mails.
The confirmation goes through omnibase's notifier, as every mail of the site does (importance "email", from the site's address).
A letter does not: omnibase's Notification builds its e-mail itself and has no way to add a header, and a letter must carry List-Unsubscribe and List-Unsubscribe-Post (RFC 8058) - what Gmail and the others ask of a sender of bulk mail, and what gives the reader the "unsubscribe" button of their mail client. So it is a TemplatedEmail sent with Symfony's mailer, from the same address the notifier uses.
Both templates are overridden from templates/bundles/NewsletterBundle/email/.
NewsletterMailer::__construct()
public function __construct(readonly MailerInterface $mailer, readonly NotifierInterface $notifier, readonly UrlGeneratorInterface $urls, readonly SettingBagInterface $settings, readonly TranslatorInterface $translator, readonly ?string $fromName = null)NewsletterMailer::confirm()
public function confirm(Subscriber $subscriber): void"Confirm your address": the link that puts it on the list.
NewsletterMailer::campaign()
public function campaign(Subscriber $subscriber, Campaign $campaign, bool $test = false): voidA letter to one subscriber, with its way out: the link in the text, and the two headers of a one-click unsubscribe. A test is marked so in its subject.
SignedTimestamp class
final class SignedTimestamp SignedTimestamp.php:10
The time the form was opened, signed with the kernel's secret: a robot cannot send an old timestamp of its own making to look patient.
"1767225600.3f9a..." - the seconds, a dot, an HMAC of them.
SignedTimestamp::sign()
public static function sign(int $time, string $secret): stringSignedTimestamp::verify()
public static function verify(?string $value, string $secret): ?intThe time it carries, or null when it was not signed with $secret.
SubscribeGuard class
final class SubscribeGuard SubscribeGuard.php:22
What stops someone using the form to flood a stranger's inbox with
confirmation mails: a second sign-up from the same address within the
flood interval, more than the limiter allows in the hour. The robots are
the form's guard's (glitchr/omnibase's option guard); with a core from
before it, the form's own trap and signed time are read here: the trap
filled (thanked and dropped, so the robot learns nothing), a form sent
faster than a person types or with a time it did not get from us. Each
answers with a reason the controller turns into a flash.
SubscribeGuard::__construct()
public function __construct(readonly SubscriberRepository $subscribers, readonly string $secret, readonly int $minDelay = 3, readonly int $floodInterval = 60, readonly RateLimiterFactoryInterface|RateLimiterFactory|null $limiter = null)SubscribeGuard::check()
public function check(FormInterface $form, Request $request): ?stringnull when the address may be signed up.
Base\Newsletter\Twig
NewsletterExtension class
final class NewsletterExtension extends AbstractExtension NewsletterExtension.php:16
What a host's own pages ask the newsletter: the sign-up form for a footer or a home page (its source: the page's key, kept with the address), and how many read the letters.
NewsletterExtension::__construct()
public function __construct(readonly NewsletterController $controller, readonly SubscriberRepository $subscribers)NewsletterExtension::getFunctions()
public function getFunctions(): array