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

Laravel Holded Laravel Package

xavicabot/laravel-holded

Laravel wrapper for the Holded API that lets you manage invoices, contacts, products, accounting entries and more from your PHP application. Clean, fluent syntax makes it easy to add invoicing and ERP features to any Laravel project.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

  • The package is a thin SDK wrapper around the Holded SaaS API, not a domain layer. v3.0 moves it toward a more conventional client architecture: BaseModule centralizes transport concerns, HoldedException normalizes the error contract, and ContactData introduces a DTO boundary at the write path. That is the right shape for a Laravel integration — modules map to API resource groups, config lives in config/holded.php, and resolution happens through the service container.
  • The BaseModule extraction is the most consequential structural change. Deduplicating HTTP logic out of DocumentModule and ContactModule means retry/backoff behavior is now uniform across resources. From a governance standpoint this is good: one place to audit timeouts, headers, and retry policy. It also means a defect in BaseModule has blast radius across every module, so the 49-test/98-assertion suite becomes the primary safety net rather than a nicety.
  • HoldedException with getStatusCode(), getResponseBody(), isRateLimited(), isServerError(), isClientError() is a genuine improvement for callers. It replaces Guzzle-exception leakage with a stable contract that application code can branch on without importing Guzzle internals. This reduces coupling to the HTTP transport and is the kind of abstraction that survives a future swap of HTTP client.
  • The DTO (ContactData) plus union acceptance (ContactData|array) is a pragmatic transitional design. It lets teams adopt validation incrementally without a hard cutover — good for adoption, mildly bad for long-term type clarity, since array remains a permanently supported escape hatch.

Integration feasibility

  • Installation is unremarkable: composer require, publish config, inject modules. No new infrastructure, queues, or external services are introduced.
  • The breaking changes are all mechanical but mandatory:
    • PHP 8.2 minimum — affects CI images, Docker base images, and any host pinned to 8.1.
    • Guzzle exceptions no longer bubble — every existing catch (GuzzleException) or generic catch (Exception) around a Holded call needs review. Silent swallowing of Exception will now mask a typed HoldedException and lose the status-code branch points.
    • Module constructors take a new $retryConfig parameter — any manual new DocumentModule(...) instantiation breaks. Container-resolved usage should be unaffected, which is an argument for auditing for manual instantiation first.
  • New capability (attachments, PDF) is additive and low-risk to adopt, but it expands the package's surface into binary transfer, which carries different failure characteristics than JSON calls.

Technical risk

  • Highest risk: automatic retries on non-idempotent writes. The release notes describe retries on 429 and 5xx with no mention of idempotency keys, request-method gating, or a retry cap. A retried POST to create an invoice can produce a duplicate document if the original request succeeded server-side but the response was lost to a 5xx or a gateway timeout. For an accounting system of record this is a data-integrity issue, not a performance one. This must be answered before enabling retries in production.
  • Retry amplification during vendor degradation. Exponential backoff on 429 and linear on 5xx is reasonable, but if defaults are aggressive (e.g., many attempts) and shared across a queue fleet, a Holded outage converts into a self-inflicted traffic spike when service resumes. Needs a documented default ceiling and per-job jitter.
  • PDF as base64 in memory. document()->pdf($id) returning base64 implies full buffering of the payload. Large documents inflate memory ~33% over raw size and can push queue workers over memory_limit. No streaming API is described.
  • Attachment uploads via multipart. No stated file-size limit, MIME restriction, or timeout handling. Large uploads interacting with retry logic is a compounding risk (retried 500MB uploads).
  • Test coverage is thin for the new complexity. 49 tests / 98 assertions across DTOs, modules, errors, and retries is a reasonable foundation but likely under-tests retry timing, exception mapping edge cases, and multipart failure paths. Verify whether retry tests use a mocked clock or sleep for real.
  • Unaddressed concerns visible from the notes: auth/token refresh behavior, request timeouts, pagination, and rate-limit header surfacing are not mentioned.

Key questions

  1. Are retries restricted to idempotent methods (GET/PUT/DELETE), or do POST creates retry? If they retry, is there an idempotency key or dedupe mechanism?
  2. What are the shipped defaults in config/holded.php — max attempts, base delay, jitter, and total elapsed cap?
  3. Is there a configurable per-request timeout, and does it differ for multipart uploads and PDF downloads?
  4. Does HoldedException::getResponseBody() redact anything, and is it safe to write to logs? Bodies can contain customer PII and invoice data.
  5. How does the package behave on 401/403 — is token/credential refresh in scope or left to the consumer?
  6. What is the maintenance commitment and Laravel version support policy? Does a 13.x release follow promptly?
  7. Is pdf() returning base64 a deliberate constraint of the vendor API, or a package choice that could become a stream?
  8. How are retry sleeps implemented — blocking sleep() in-process (unacceptable for web requests) or Laravel's Sleep/queue release?

Integration Approach

Stack fit

  • Targets Laravel 11 and 12 with illuminate/support ^11.0|^12.0 and PHP ^8.2. This matches a currently supported Laravel footprint and drops the EOL 8.x/10.x lines cleanly — a defensible support matrix, provided the team's apps are already on 11+.
  • Fits both web-request and queue-worker contexts, but retry behavior differs materially between them. Prefer invoking Holded writes from queued jobs with job-level retry and release() backoff rather than in-process HTTP retries, and treat the package retries as a secondary layer.

Migration path (v2 → v3)

  1. Inventory. Grep for DocumentModule, ContactModule, GuzzleHttp, GuzzleException, and new <Module>(. Classify container-resolved vs. manually constructed call sites. This step determines the real scope; everything after it is mechanical.
  2. Platform prep. Raise PHP to 8.2 across local, CI, and runtime images. Confirm no transitive dependency pins illuminate/support below 11.
  3. Upgrade in a branch. Bump the package constraint, publish/merge the new config/holded.php using vendor:publish --force into a diff so retry defaults are visible and reviewed rather than silently accepted.
  4. Fix error handling. Replace Guzzle catches with HoldedException; use isRateLimited() / isServerError() / isClientError() and getStatusCode() for branching. Audit bare catch (Exception) blocks specifically — they will now absorb the new typed exception and hide retry exhaustion.
  5. Fix constructors. Route manual instantiations through the container or a small factory so $retryConfig is injected from config in one place.
  6. Adopt DTOs opportunistically. Convert existing array payloads to ContactData on a per-call-site basis; the ContactData|array union means this is not a blocking step.
  7. Enable new features behind flags. Attachments and PDF download should ship dark, then be enabled per feature.

Compatibility

  • Verify coexistence with packages that also constrain illuminate/support (e.g., older Laravel Nova, legacy auth packages). Composer resolution failures here surface late and are the most common surprise in this class of upgrade.
  • If multiple services consume this package, version-skew is a real concern: a v3 consumer and a v2 consumer in the same monorepo means two different error contracts. Standardize on one version before rollout.

Sequencing

  • Do the PHP/Laravel bump as its own PR, merged and green, before touching the package. Mixing platform upgrade with an SDK upgrade makes rollback ambiguous.
  • Roll out in this order: dev → staging against a Holded sandbox account → production read-paths only → production write-paths with retries disabled → retries enabled.
  • Keep retries off in production until the idempotency question is answered. This is the single most important sequencing decision in this release.

Operational Impact

Maintenance

  • Small, single-maintainer-style package risk is the default assumption until proven otherwise. A 3.0.0 with documented breaking changes and a real test suite is a positive signal of active stewardship, but the team should keep the ability to fork and pin. Pin to an exact minor range (^3.0) and treat minor bumps as reviewable, not auto-mergeable.
  • Breaking changes in a package that touches an accounting system of record are higher-stakes than the version number suggests. Maintain a short internal changelog mapping package versions to app releases.

Support

  • HoldedException is a net operational win: errors are now classifiable, so alerting can distinguish rate-limit backpressure from client errors (bad payload — page someone) from server errors (vendor outage — do not page).
  • The retry layer introduces a new failure category that did not exist before: "retries exhausted." This needs its own log signature and runbook entry, otherwise it presents as a generic 5xx and gets mis-triaged.
  • getResponseBody() in exception messages is convenient and dangerous. Any log shipping or error-tracker integration must confirm vendor bodies are scrubbed or access-controlled, since invoice and contact payloads carry customer PII.

Scaling

  • Throughput is bounded by the vendor, not the package. Retries reduce transient failure rates but increase total request volume during degradation — the two effects must be measured together.
  • PDF base64 and multipart uploads scale with document size, not request count. Worker memory and HTTP timeouts become the limiting factor, not API quota. Size the queue workers accordingly, or isolate binary operations into their own queue with larger limits and longer timeouts.
  • Config is global (config/holded.php), so per-tenant or per-account retry tuning is not possible without multiple container bindings. Flag this if the app serves multiple Holded accounts with different limits.

Failure modes

Failure Symptom Mitigation
Retried non-idempotent POST Duplicate invoices/contacts Disable write retries until idempotency is confirmed; reconcile via Holded number sequences
Retry storm after outage 429s persist long after recovery Cap attempts and total elapsed; add jitter; use queue release()
Large PDF download Worker OOM kill Dedicated queue, memory headroom, size guard before fetch
Multipart upload retried Duplicated or orphaned attachments, slow jobs Restrict retries to 429 only for uploads; enforce size/MIME limits
Swallowed HoldedException Silent data loss with green dashboards Audit bare catch (Exception); lint for it
Credential expiry (401) Uniform auth failures across all modules Confirm refresh behavior; surface 401 distinctly in alerts
Vendor response body in logs PII exposure Scrub or restrict getResponseBody() at the logging layer

Ramp-up

  • Roughly half a day for a developer already familiar with Laravel HTTP clients and the Holded API; the DTO and exception APIs are self-explanatory. Budget an additional half-day for anyone who owns error handling and alerting, since the failure taxonomy changed.
  • QA needs a Holded sandbox account and a scripted set of failure injections (429, 500, timeout, oversize upload) to validate retry behavior. Without a sandbox, retry validation degrades to unit tests only, which is insufficient for the idempotency question.
  • Highest-value onboarding artifact is not package docs but a one-page internal runbook: which exceptions page whom, how to disable retries via config, and how to reconcile duplicates in Holded.
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.
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
ably/pubsub-server
mstfkhazaal/defender
mfonte/base62x
karnoweb/metapilot
tobya/bcsapiwrapper
swalker2/cpanel
vish4395/laravel-translation-manager
morningtrain/economic
teksite/systeminfo-laravel
atomcms/voting
run6/weather
siokas/laravelembeddirectives
adamlien/pkuuids
gilsonsouza/lara-doc-generator