composer/spdx-licenses
Standalone PHP library for SPDX license data and validation. Look up licenses and exceptions by identifier or name, check OSI approval and deprecation status, and validate SPDX license expressions. Lightweight and dependency-free, originally extracted from Composer. Requires PHP 5.3.2+.
Install via Composer — this is a data + validation library with zero dependencies, so it won't fight your Laravel stack:
composer require composer/spdx-licenses
Then instantiate and hit the two methods you'll actually use 90% of the time:
use Composer\Spdx\SpdxLicenses;
$spdx = new SpdxLicenses();
// 1. Is this string a legal SPDX expression?
$isValid = $spdx->validate('MIT OR Apache-2.0'); // true
$isValid = $spdx->validate('MIT OR'); // false
// 2. Canonicalise / look up a single identifier
$license = $spdx->getLicenseByIdentifier('MIT');
// ['name' => 'MIT License', 'identifier' => 'MIT', 'osiApproved' => true, ...]
Look first at the SpdxLicenses class itself (Composer\Spdx\SpdxLicenses) — it's a small surface area, and the README maps every public method. The bundled resources/ directory (JSON files of the SPDX list) is where the actual data lives; you should almost never touch it directly.
Your first real use case will be a form that accepts a license: a package manifest editor, an onboarding screen for partner integrations, or a field on a "third-party component" model. Wire validate() to that input and store the canonical getIdentifierByName()/getLicenseByIdentifier() result rather than the raw user string. That single change is what makes downstream filtering (WHERE license IN ('MIT','Apache-2.0')) and reporting possible.
1. Form request validation (the bread-and-butter pattern).
Wrap validate() in a Laravel Rule. This keeps controllers clean and gives you a reusable validator across requests:
namespace App\Rules;
use Composer\Spdx\SpdxLicenses;
use Illuminate\Contracts\Validation\Rule;
class SpdxExpression implements Rule
{
public function passes($attribute, $value): bool
{
return (new SpdxLicenses())->validate($value);
}
public function message(): string
{
return 'The :attribute must be a valid SPDX license expression.';
}
}
Then 'license' => ['required', new SpdxExpression()] on any DTO. Because identifiers are case-insensitive since 1.3.0, you don't need to pre-lowercase.
2. A thin application service so you're not new SpdxLicenses() everywhere.
Bind it as a singleton in a service provider (it's cheap but the full license list is loaded lazily), then depend on a facade-like wrapper:
// App\Services\LicenseService
public function isAcceptable(string $expression): bool
{
if (!$this->spdx->validate($expression)) {
return false;
}
// Policy layer: reject deprecated IDs and unapproved variants.
foreach ($this->identifiersIn($expression) as $id) {
if ($this->spdx->isDeprecatedByIdentifier($id)) {
return false;
}
}
return true;
}
3. Dependency triage / SBOM export.
When generating composer.json for downstream packages, or building a CycloneDX component list, use getLicenseByIdentifier() to resolve the human-readable name and OSI flag once, at write time:
$result = $spdx->getLicenseByIdentifier($row->license);
if ($result === null) {
// Unknown — fall back to validating the raw expression, or flag for review
}
Store the canonical identifier for joins and the name for display. This is the single highest-leverage thing for procurement questionnaires and audit reports.
4. CI checks with a warning-first policy.
Use the library in your build script (not just runtime) to gate PRs. Start with warnings for a few weeks, then flip to blocking at release, then at PR — otherwise you'll get a spike of false positives that erodes trust in the check. The isDeprecatedByIdentifier() check alone catches most stale metadata.
5. Policy engine integration.
Wrap OSI/deprecation checks into a LicensePolicy object so the product decision (block copyleft? allow only OSI?) lives in one place and can change per tenant or package tier without touching validation code.
validate() is expression-level, not identifier-level. MIT OR Apache-2.0 is valid even if you've never enumerated the components. If your business rule is "must be a single known license," check getLicenseByIdentifier() and validate — don't rely on validate() alone.getLicenseByIdentifier() returns null for unknown identifiers, and returns a null/array depending on version quirks for deprecated IDs — always null-check rather than assuming an array is returned.isDeprecatedByIdentifier('GPL-2.0') returns true, but the expression still validates. Decide explicitly whether deprecated IDs are blocked — the library won't do it for you.+ operator (GPL-2.0+) and WITH clauses are easy to mis-validate manually. This is the strongest argument for not hand-rolling validation; the regex implementation (since 1.0.0) handles parens/WITH/+ correctly.SpdxLicenses::LICENSES_FILE, EXCEPTIONS_FILE, and getResourcesDir() exist if you ever need to point at a custom snapshot — but treat the data as append-only and version-controlled, not something you edit in place.false path and test against the raw SPDX spec site — most failures are typos (Apache 2.0 vs Apache-2.0) or free-text (MIT License is a name, not an identifier; use getIdentifierByName()).How can I help you explore Laravel packages today?