ticketpark/pingen-bundle
Symfony bundle that integrates the Pingen API for sending physical letters and postcards directly from your application. Configure your credentials, then generate, upload, and dispatch mail through simple services, ideal for automating transactional or bulk postal correspondence.
This bundle is a Symfony 2 wrapper around the ticketpark/pingen PHP client, which talks to pingen.com — a print-and-mail service that turns a PDF plus an address into a physically posted letter. The bundle's only real job is registering that client in the DI container from a small config block, so treat it as a service-provider shim, not a framework integration.
Minimal setup in a Symfony 2 project:
composer require ticketpark/pingen-bundle
// app/AppKernel.php
public function registerBundles()
{
$bundles = [
// ...
new Ticketpark\PingenBundle\TicketparkPingenBundle(),
];
}
# app/config/config.yml
ticketpark_pingen:
client_id: "%pingen_client_id%"
client_secret: "%pingen_client_secret%"
# app/config/parameters.yml — keep credentials out of VCS
parameters:
pingen_client_id: "%env(PINGEN_CLIENT_ID)%"
pingen_client_secret: "%env(PINGEN_CLIENT_SECRET)%"
Do not trust those key names blindly — the bundle predates standardised config and was iterated on. Open DependencyInjection/Configuration.php in the installed package first; that file is the authoritative list of supported keys, and the DependencyInjection/*Extension.php file tells you the exact service ids it registers. Then confirm at runtime:
php app/console container:debug | grep -i pingen
First use case: send an invoice PDF. Load the client service from the container, create a letter, attach the PDF, set the recipient address, and send. Do this from a console command or a queue worker, never from a controller that a user is waiting on — you are making a synchronous HTTP call to a third-party API that can take several seconds and will fail occasionally.
If container:debug shows only one service and it's the client, that's the whole surface area of this bundle. Everything else you do is the ticketpark/pingen library itself, so read its README and src/Endpoints/* classes — that's where the real API lives.
Wrap the client in your own service. This is the single highest-value pattern for this package, because the package is archived and you will migrate off it. Never type-hint the vendor client in your controllers or domain services. Introduce a narrow interface and one adapter:
// AppBundle/Pingen/LetterSenderInterface.php
interface LetterSenderInterface
{
/**
* @return string The provider's letter id, for later status polling.
*/
public function send(Letter $letter): string;
}
// AppBundle/Pingen/PingenLetterSender.php
class PingenLetterSender implements LetterSenderInterface
{
private $client;
private $logger;
public function __construct($client, LoggerInterface $logger)
{
$this->client = $client;
$this->logger = $logger;
}
public function send(Letter $letter): string
{
// Endpoint/method names vary by library version — check
// vendor/ticketpark/pingen/src/Endpoints before copying this.
$endpoint = $this->client->letters();
$created = $endpoint->createLetter([
'address' => $letter->getRecipientAddress(),
'country' => $letter->getCountryCode(),
]);
$endpoint->uploadFile($created->getId(), $letter->getPdfPath());
$endpoint->sendLetter($created->getId());
return $created->getId();
}
}
Register it as a service with the bundle's client id injected, and mark it public: false.
Persist the provider id, not the outcome. Store pingen_letter_id, the requested sent_at, and a local status column on your entity. Letter delivery is asynchronous and postal — the API's "sent" means "accepted for printing". Status transitions arrive via polling the letter endpoint (or, on newer Pingen versions, webhooks). Your domain should own a LetterStatus enum and map the provider's strings onto it, so a future migration to Pingen's v2 API doesn't ripple through your codebase.
Queue the send. A SendLetterJob with the local letter id, processed by a worker, gives you retries, backoff, and a dead-letter path. Idempotency matters: if you retry after a timeout you may create a duplicate physical letter, which costs real money. Guard it by persisting the pingen_letter_id immediately after the create call and checking for it before retrying.
Group multi-page documents yourself. Pingen bills per letter/page and enforces PDF constraints. Build the PDF locally (dompdf, wkhtmltopdf, TCPDF) and validate page count, A4 dimensions, and embedded fonts before you ever hit the API — a failed upload after a slow round-trip is the most annoying failure mode.
Test with a fake. Because the bundle only exposes the configured client, the cleanest testing story is to inject a stub LetterSenderInterface for functional tests, and in unit tests mock the vendor client or its endpoints. If the library supports pointing at a non-production base URL, wire that in config_test.yml via the bundle's endpoint/host config key (again: check Configuration.php); otherwise keep all Pingen calls behind the interface and stub it.
Legacy integration in a modern app. If you're on Symfony 3/4+ and must use this bundle, you'll be fighting AppKernel conventions and old constraint files. A better move is to drop the bundle entirely and register the underlying ticketpark/pingen client as a plain service in services.yml, configured from environment variables. You lose nothing — the bundle adds almost no behaviour.
composer.json, commit composer.lock, and expect composer update to break things.DependencyInjection/Configuration.php and the extension class are your only sources of truth, plus container:debug for service ids. Budget an hour to read those files before writing integration code.parameters.yml. Use %env()% (or a parameters.yml.dist + local override) so client secrets never land in git.info on create and on every status transition.ticketpark/pingen library registered as a plain service, or Pingen's official v2 SDK. The LetterSenderInterface wrapper above makes that a one-class change.How can I help you explore Laravel packages today?