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

Appmanager Laravel Package

hulkapps/appmanager

HulkApps AppManager is a Laravel/PHP package for application management, offering scaffolding for app settings and administrative workflows. It has minimal community traction, so review the repository and documentation before using it in production.

View on GitHub
Deep Wiki
Context7

Getting Started

hulkapps/appmanager is a HulkApps-published SDK (app-manager-sdk) for the "many Shopify-style apps, one Laravel codebase" problem: registering app definitions, resolving which app the current request belongs to, and — as of 3.7.0, unambiguously — owning the billing surface: /plans, per-plan discount resolution, and a ChargeController::process() that actually creates the charge. Treat the README as a hypothesis and src/ as the truth; there are still zero dependents and no public discussion, but 3.7.0 ships a real changelog and a GitHub compare link, so the package is maintained rather than abandoned.

Minimal path in:

  1. composer require hulkapps/appmanager:^3.7 and read the require block in the package's composer.json. Confirm the supported Laravel/PHP matrix before committing — the SDK is now at 3.x on a 3.6.2 → 3.7.0 cadence, so minors are moving.
  2. Check for extra.laravel.providers / extra.laravel.aliases in the package composer.json. If present, the provider auto-registers; if not, add it to bootstrap/providers.php (Laravel 11+) or config/app.php.
  3. php artisan vendor:publish --provider="HulkApps\AppManager\AppManagerServiceProvider" and read the published config end to end. Config keys are the de-facto API surface.
  4. Open vendor/hulkapps/appmanager/src and identify five things: the manager class, the contract it resolves from the container, any middleware, ChargeController, and the client methods getPlans() / getAutomaticDiscount().

First use case to validate is no longer just "resolve two apps" — it's display/charge parity. Call getPlans() with a discount_code and a real shop_installed_at, confirm the injected per-plan discount, then drive ChargeController::process() for that same plan and assert the amount charged matches the amount displayed. That is the promise 3.7.0 makes ("the billing-page display and the actual charge share one verdict") and the only test that tells you whether the abstraction holds. Second, repeat both calls from an artisan command — if resolution only works from HTTP context, you've found the constraint that will bite you in queued jobs and scheduled billing.

Two prerequisites before you trust a green test suite: the backend must have promotional-discount targeting (MGR-97) deployed, and the tag must actually exist on Packagist. Pin the version and commit composer.lock.

Implementation Patterns

Resolve, don't instantiate. Wrap the manager behind a single injectable boundary in your own code — an AppContext service bound in your own provider that delegates to the package. Day-to-day you depend on your interface, so swapping the SDK out (still plausible at zero dependents) is a one-file change.

// app/Providers/AppServiceProvider.php
$this->app->singleton(AppContext::class, fn ($app) => new SdkAppContext(
    $app->make(\HulkApps\AppManager\AppManager::class)
));

Middleware for request-scoped resolution. If the package ships middleware, register it on the webhook/API group, not globally. If it doesn't, write a thin middleware that reads the tenant identifier (header, host, or signed payload), calls the resolve method, and stores the result in a scoped() binding. Keep the resolution rule in one class — you will change it.

Config-driven app registry. App definitions (key, slug, credential source, scopes, webhook topics, default plan catalog) belong in config/appmanager.php; secrets come from env() or a secrets manager. Use the DB only for per-install state: tokens, current plan, installed_at.

Thread the context into jobs explicitly. Never let a queued job re-resolve "the current app" from ambient state.

final class SyncOrders implements ShouldQueue
{
    public function __construct(private string $appKey, private int $shopId) {}

    public function handle(AppContext $ctx): void
    {
        $ctx->forApp($this->appKey, $this->shopId)->run(fn () => /* work */);
    }
}

Discount resolution: one verdict, two call sites. 3.7.0 makes discounts a two-phase operation — display (via getPlans()) and charge (via ChargeController::process() / getAutomaticDiscount()). Your job is to guarantee the two agree, because the SDK only guarantees it while the backend is reachable.

  • Always forward discount_code and shop_installed_at into getPlans(). The release is explicit that this is what lets /plans resolve and inject the winning per-plan discount with no extra round trip. Omitting shop_installed_at silently degrades targeting (new-merchant rules, cohort rules) rather than erroring.
  • Source shop_installed_at from a single column that is written once at install and never recomputed from a webhook timestamp. Skew between your value and App Manager's is the most likely cause of "the discount showed but didn't apply."
  • Persist the verdict. When /plans returns a discounted price, store {plan, discount_id, value, type, cycles, computed_at} on the install record. At charge time, compare what the SDK is about to charge against that row and alert on mismatch. This is your only defence against the fail-safe divergence described below.

Automatic vs coded discounts. A discount flagged is_automatic applies with no code. App Manager picks the winner among automatic-plus-any-code by priority, with automatic breaking ties. Practical consequences:

  • Never compute "best discount" in your own code. Ask the SDK, and render whatever it returns. Duplicating priority logic guarantees drift.
  • Don't gate the promotion UI on discount_code being present — automatic discounts render for merchants with no code at all. Test the no-code path explicitly.
  • Test the tie: two candidate discounts of equal priority, one automatic. Assert the automatic one wins, and assert the same discount wins on both the /plans and charge paths.

Honour "upgrades only" as a data condition, not a UI condition. ChargeController::process() applies the per-plan value/type/cycles for the chosen plan and only applies the discount when the new plan costs more than the merchant's current plan. So a downgrade, or a lateral change at equal price, is deliberately undiscounted. Build the pricing page to reflect that: if you render a struck-through price on a downgrade, you're promising a discount the charge path will refuse.

Treat cycles as a subscription-lifecycle concern. A discount scoped to N cycles expires. Your renewal, upgrade, and dunning flows all need to know cycle counts, and the SDK is not going to reconcile that for you. Store the discount's cycle budget alongside the plan and decrement it on your own renewal events.

Wrap side effects in idempotency. Any webhook handler or charge the SDK dispatches into should be idempotent — dedupe on the provider's event ID in your own table before acting. Do not assume the package does this. This applies doubly to process(): a retried charge job must not double-charge a merchant.

Test against a fake, not the container. Bind a fake AppContext in tests and assert branching per app, per plan, per discount state. Keep one or two integration tests that hit the real SDK — and make one of them the display/charge parity test — so an upstream change surfaces in a narrow place.

Extension points, in priority order: a contract you can rebind, events on install/uninstall/charge/discount-applied, a Macroable manager, publishable migrations/stubs. ChargeController is a shipped controller — if you need to change charge behaviour, subclass or rebind rather than editing vendor/. Anything published into your app is yours; anything in vendor/ is not.

Gotchas and Tips

  • The fail-safe carve-out is the headline risk. Automatic and targeted discounts are live-path only and deliberately withheld from the fail-safe backup. During an App Manager outage those discounts are neither shown nor charged — merchants silently pay full price. Coded discounts still fail safe as before. This is a business incident, not a bug: you need (a) monitoring that distinguishes "no discount because ineligible" from "no discount because backend down", (b) a documented credit/refund runbook, and (c) the persisted-verdict comparison above so you can find affected merchants after the fact.
  • Backend version skew. 3.7.0 requires App Manager with MGR-97 deployed. An SDK upgrade without the backend deploy gives you endpoints that resolve nothing. Coordinate the two, and feature-flag the promotional UI until you've confirmed the backend answers /automatic-discount correctly in staging.
  • Upgrades-only is easy to misread. The discount is suppressed when the new plan doesn't cost more than the current one. If your pricing page computes "more expensive" differently (currency, annualised vs monthly, add-ons) from the SDK, you'll advertise a discount that process() won't apply. Compare like the SDK compares.
  • getPlans() is a per-merchant, per-moment call. The response embeds shop_installed_at, code, and resolved discount. Do not put it behind a long TTL or a shared cache key that lacks shop + code. Cache per merchant with a short TTL if you must.
  • Config caching. After editing config/appmanager.php, run php artisan config:clear. A stale cached config in a container is the classic "works locally" failure.
  • Console and queue contexts. Managers that resolve app identity from the request return null or throw under artisan and in workers. Always pass the app key explicitly; ambient resolution is an HTTP-only convenience.
  • Singleton leakage in Octane. If the manager is a singleton holding current-app or credential state, it bleeds between requests. Register scoped(), or require every public method to take the app key as an argument.
  • Credentials read at call time. Don't capture tokens in a constructor — a rotation shouldn't need a deploy or a worker restart.
  • Alias collisions. A vendor facade aliased AppManager is a coin flip against your own classes. Prefer constructor injection; it also makes faking trivial.
  • Debugging. Log the resolved app key at every entry point and Log::debug() around every SDK boundary for the first two weeks. For discount issues, log the full discount verdict — discount_id, value, type, cycles, is_automatic, and whether it came from live path or fail-safe. When something misbehaves, the question is always "which app, which discount, which path."
  • Composer resolution. If the package pins an older illuminate/support, composer require can silently downgrade a transitive dependency. Run composer why hulkapps/appmanager and diff composer.lock after install.
  • Version policy. 3.7.0 adds no config or fail-safe schema change, so the upgrade itself is low-risk — but it changes billing behaviour, which means a minor bump can change what merchants are charged. Pin ^3.7, commit the lock, and read the changelog on every bump. Zero dependents still means zero community bug reports; budget for a fork (MIT, small src/).
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/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
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route