Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cups Ipp Laravel Package

smalot/cups-ipp

PHP library for communicating with CUPS print servers over IPP. Send documents to printers, manage and monitor print jobs, query printer status and attributes, and control queues directly from your PHP application using a lightweight Internet Printing Protocol client.

Deep Wiki
Context7

Getting Started

Install and sanity-check compatibility first. The last tagged release is from May 2020, so before writing any code run composer require smalot/cups-ipp on your actual PHP version and confirm the Guzzle constraint resolves. On PHP 8.2+ you will likely see deprecation noise from the old Guzzle 6 constraint; if so, pin Guzzle 7 via a fork or replace rather than fighting it later.

Where to look first. The package is small and thinly documented, so the README is only a starting point. Read these in order:

  1. src/Manager/PrinterManager.php and src/Manager/JobManager.php — the entire day-to-day API surface lives here.
  2. src/Model/Printer.php and src/Model/Job.php — what you set and what you get back.
  3. src/Builder/RequestBuilder.php / AttributeBuilder.php — when an attribute "doesn't work," this is where you find out why.

Minimum viable wiring (nothing is auto-discovered; there is no Laravel service provider):

use Smalot\Cups\Builder\Builder;
use Smalot\Cups\Transport\Client;
use Smalot\Cups\Manager\PrinterManager;
use Smalot\Cups\Manager\JobManager;

$client         = new Client(new Builder()); // second arg = Guzzle options in most versions
$printerManager = new PrinterManager($client);
$jobManager     = new JobManager($client);

foreach ($printerManager->findAll() as $printer) {
    echo $printer->getName(), PHP_EOL;
}

First use case: list the queues, then print a PDF. Don't start with the printer your users care about. Stand up a cups-pdf queue (or run CUPS in Docker with -p 631:631) so a bad job costs you a file in /var/spool/cups-pdf instead of 200 sheets of paper. Verify from the shell with lpstat -t and http://localhost:631 before blaming the library.

$printer = $printerManager->findByUri('ipp://localhost:631/printers/PDF');

$job = new \Smalot\Cups\Model\Job();
$job->setName('smoke-test.pdf');
$job->setContent(file_get_contents(storage_path('app/test.pdf')));
$job->setContentType('application/pdf');

$jobManager->send($printer, $job);

If findAll() returns printers but send() throws, the problem is almost always content type, authentication, or a URI scheme mismatch (ipp:// vs ipps://) — not your payload.

Implementation Patterns

Bind everything as singletons in a service provider. The Builder and Client are stateless-ish and constructing a Guzzle client per call is wasteful.

// config/printing.php
return [
    'uri'      => env('CUPS_URI', 'ipp://localhost:631/printers/PDF'),
    'timeout'  => env('CUPS_TIMEOUT', 30),
    'verify'   => env('CUPS_VERIFY_TLS', true),
];
$this->app->singleton(Client::class, fn () => new Client(new Builder(), [
    'timeout' => config('printing.timeout'),
    'verify'  => config('printing.verify'),
]));
$this->app->singleton(PrinterManager::class, fn ($app) => new PrinterManager($app->make(Client::class)));
$this->app->singleton(JobManager::class,     fn ($app) => new JobManager($app->make(Client::class)));

Hide it behind your own interface. Because the package is GPL-2.0, unmaintained since 2020, and talks a 1990s protocol, you want exactly one place to swap it out. Define App\Contracts\PrinterGateway with printers(): array and submit(string $queue, string $pdf, array $options): PrintReceipt, and implement it with this library. Everything else in the app injects the interface.

Never print synchronously in a request. send() blocks on a network round trip to cupsd, and cupsd can block on a slow printer with a large spool. Dispatch a queued job instead:

final class SendToPrinter implements ShouldQueue
{
    use Queueable;
    public int $tries = 3;
    public array $backoff = [5, 30, 120];

    public function __construct(public int $printAttemptId) {}

    public function handle(PrinterGateway $gateway): void
    {
        $attempt = PrintAttempt::findOrFail($this->printAttemptId);
        if ($attempt->state !== 'pending') return; // idempotency guard
        $gateway->submit($attempt->queue, Storage::get($attempt->path), $attempt->options);
        $attempt->update(['state' => 'submitted', 'submitted_at' => now()]);
    }
}

Keep a local print_attempts table. The CUPS-side job ID is the only handle you get back; store it alongside queue, document_hash, state, cups_job_id, last_error. This is what makes "did it print?" answerable, and it gives you a cheap dedupe key on document_hash so a retried queue job doesn't produce two copies.

Poll job state with a scheduled command, not inside the print job. Submit, persist the job URI/ID, then have a printing:poll command every minute that fetches attributes and advances your state machine (submitted → processing → completed | canceled | aborted). Do not hold a worker open waiting for paper.

Map business options to IPP attributes explicitly. Copies, duplex, media size, page ranges — whatever your app exposes, translate in one place and validate against a whitelist. Attributes are hyphenated (copies, sides=two-sided-long-edge, media=A4, page-ranges=1-3) and the Job model exposes addAttribute() / getAttribute() style helpers; confirm exact names against AttributeBuilder in your installed version.

Reporting is a free win. PrinterManager + printer attributes give you printer-state, printer-state-reasons, and marker-levels for toner/ink dashboards. Poll those on a schedule and alert on marker-levels thresholds or media-empty — this is usually the highest-value thing the package does after printing.

Gotchas and Tips

The license is the biggest risk, not the code. GPL-2.0 is copyleft. Shipping this inside a proprietary Laravel SaaS is a legal question, not a technical one — get sign-off early or plan an exit. The practical mitigation is the PrinterGateway interface, so the library never leaks past one adapter.

HTTP 200 does not mean "printed." Check the IPP status code and job-state-reasons. Jobs routinely land in held or aborted with a perfectly successful HTTP response. Always surface job-state-reasons in your error messages; it's the only thing that tells you why (e.g. document-format-not-supported, cups-missing-filter, media-empty).

Content type must match a CUPS filter. application/pdf is the safe default. application/octet-stream will fail or be rejected by many queues. If the queue is configured for PostScript, sending PDF only works if cups-filters is installed — verify with lpstat -t and test a real job through lp before you debug PHP.

Restrict the printer URI from user input. Every send() is an outbound HTTP request to whatever host is in the URI. Letting a tenant supply ipp://10.0.0.5:631/... is an SSRF-adjacent hole that also lets them spam internal hardware. Keep queue URIs in config/admin-only state and resolve them by key.

Memory: setContent() takes the whole document as a string. No streaming. A 500 MB PDF becomes a string in PHP and gets base64-inlined into the request body. Cap document size, prefer 1–5 MB PDFs, and offload anything larger to a lp/CUPS-REST sidecar. Watch memory_limit on queue workers specifically.

Authentication and TLS are Guzzle's problem, not the package's. For a remote or authenticated cupsd, pass Guzzle options (auth, verify, proxy, cert) into the Client constructor. ipps:// on 631 with a self-signed cert will fail loudly unless you set verify => false in dev only. In Docker, remember cupsd listens on localhost by default — you need Listen 0.0.0.0 and an Allow rule in cupsd.conf, plus host.docker.internal:631 from the app container.

Printer names with spaces or unicode must be URL-encoded in the URI path, or findByUri() silently returns nothing. findAll() is your friend for discovering the exact canonical name.

Testing. There's no fake. Inject a mocked Guzzle handler into the transport (if your installed version allows it) or, more robustly, mock your own PrinterGateway in tests and cover this adapter with a handful of integration tests against a real CUPS container. Don't try to unit-test IPP response parsing — you'll spend a day on it and learn nothing about your app.

Extension points are thin, so patch deliberately. If you need a request attribute the builders don't emit, the realistic options are (a) fork and add it in AttributeBuilder, or (b) keep a small fork vendored and track it. Avoid composer.json repositories hacks that two future developers will not understand. Document the fork's purpose in the adapter class docblock.

Cancel and retry are not free. There is no built-in dedupe. If a worker times out mid-submit, you have an orphan job on the queue. Before resubmitting on retry, list the printer's jobs by name and check for a matching job-name; a duplicate print is the single most common production complaint with this library.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin