intracto/smartcode-bundle
Symfony bundle for generating unique, human-friendly codes such as vouchers, coupons, or activation keys. Configure character sets, length, and patterns, then generate codes in PHP services or directly in Twig templates.
Read this first: it is a Symfony bundle, not a Laravel package. There is no SmartCodeServiceProvider, no config/smartcode.php, and Laravel's package discovery will ignore it. composer require intracto/smartcode-bundle in a Laravel app will install the code but nothing will be wired up. Plan for one of two shapes:
POST /licenses and POST /licenses/verify, called from Laravel over HTTP. Keeps the bundle in its natural habitat and isolates the licensing secret from your app servers.app/Support/Licensing/ and drop the Symfony DependencyInjection/ and Resources/config/ folders. Do this only if the underlying intracto/smartcode logic is a single self-contained class — read the source before committing.Minimal steps for the Symfony route:
composer require intracto/smartcode-bundle
// config/bundles.php
Intracto\SmartCodeBundle\SmartCodeBundle::class => ['all' => true],
Generate the keypair before anything else — the private key signs the payload into a license string, the public key verifies it and is the only half that ships with the licensed software:
openssl genrsa -out config/licenses/private.pem 4096
openssl rsa -in config/licenses/private.pem -pubout -out config/licenses/public.pem
Then look at the source before writing config. With 4 stars and 0 dependents there is no README you can trust and no blog posts to copy. Do this in order:
vendor/intracto/smartcode-bundle/DependencyInjection/Configuration.php # the real config tree
vendor/intracto/smartcode-bundle/Resources/config/services.xml # the real service IDs
bin/console debug:config smart_code
bin/console debug:container --show-private | grep -i smartcode
Your first use case should be the narrowest one that proves the round trip: sign a single string (a domain, e.g. acme.example.com), verify it in a second process that has only the public key, and print valid/invalid. Do not build an issuance endpoint until that round trip works, because most failure modes in this package are key-format problems, not logic problems.
Separate the issuer from the verifier at the process level, not just the class level. The whole security model collapses if the private key path is reachable from the licensed application. In Laravel terms: the private key lives in the license server's .env/secrets manager; the public key is a constant baked into the distributed customer artifact. Never put the private key in the app that ships to customers, and never let it appear in a Laravel config cache dump.
Issuance endpoint (Symfony side) — keep the payload canonical:
#[Route('/licenses', methods: ['POST'])]
public function issue(Request $request, LicenseIssuer $issuer): JsonResponse
{
$payload = $request->toArray()['payload']; // e.g. ['domain' => 'acme.example.com', 'tier' => 'pro']
ksort($payload); // canonical ordering is on you
$payload = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
return $this->json(['license' => $issuer->generate($payload)]);
}
Laravel-side client — wrap it, don't scatter it:
// app/Services/Licensing/LicenseClient.php
final class LicenseClient
{
public function issue(array $payload): string
{
$payload = Arr::sortRecursive($payload);
return retry(3, fn () => Http::baseUrl(config('services.licensing.url'))
->timeout(5)
->post('/licenses', ['payload' => $payload])
->throw()
->json('license'), 200);
}
}
Verification belongs in a middleware, not in a controller. Offline verification is cheap (one signature check), so it can run per-request on the license-bound endpoints, but cache the result for the request lifetime:
// app/Http/Middleware/EnsureValidLicense.php
public function handle(Request $request, Closure $next)
{
$license = config('licensing.key');
$expected = ['domain' => $request->getHost(), 'tier' => config('licensing.tier')];
if (! app(LicenseVerifier::class)->verify($license, Arr::sortRecursive($expected))) {
abort(403, 'Invalid or mismatched license');
}
return $next($request);
}
Bind the payload to something that changes with the customer. A license with an empty or constant payload is a license you have given to the entire internet. Domain, instance UUID, tier, and an expiry timestamp are the useful fields. Including expiry lets you do time-limited trials without implementing revocation.
Version the payload schema. {"v": 1, "domain": ...} costs nothing now and lets you add fields later without invalidating every license already issued. Without it, every payload change is a mass re-issue event.
Store issued licenses as rows, not as a blob on the customer record. A licenses table (id, customer_id, payload_json, license_string, issued_at, revoked_at) gives you re-issue, audit, and a manual revocation path even though the algorithm itself is offline-only.
BEGIN PRIVATE KEY); older PHP/phpseclib paths expect PKCS#1 (BEGIN RSA PRIVATE KEY). If signing throws, convert with openssl rsa -in private.pem -traditional -out private-pkcs1.pem and retry before you debug anything else.json_encode key order, trailing newlines, float formatting (1.0 vs 1), and unicode escaping all change the bytes and invalidate the signature. ksort + JSON_UNESCAPED_SLASHES on both sides, or the license fails only for some customers and you'll chase it for a week.+ and /; if the license travels through a URL path, a query string, or an email client, it will be mangled. Use base64url or transport it in a JSON body, and validate the string round-trips locally before shipping.composer.json for the symfony/framework-bundle constraint — it likely pins ^5.4|^6.0 and will fight a modern Symfony. Pin the bundle version and treat upgrades as a manual project, not a composer update.openssl dgst -verify call outside PHP. If OpenSSL succeeds and the bundle fails, the bug is in your payload serialization, not in cryptography.services.xml, you can override the service definition in your Symfony app's services.yaml to swap in your own implementation (e.g. Ed25519 instead of RSA) without forking. That is the cleanest escape hatch when RSA-4096 signatures make your license strings unmanageably long.How can I help you explore Laravel packages today?