daikazu / asi-smartlink
A typed PHP client for the ASI Smartlink REST API, built on Saloon, with fluent query builders, immutable DTOs, typed exceptions, and optional Laravel integration.
Fund package maintenance!
Requires
- php: ^8.3
- saloonphp/saloon: ^4.0
Requires (Dev)
- illuminate/contracts: ^12.0||^13.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^11.0.0||^10.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- saloonphp/laravel-plugin: ^4.3
Suggests
- illuminate/support: Required only to use the Laravel integration (service provider, facade, auto-published config).
- saloonphp/laravel-plugin: Enables Saloon's Laravel features such as Saloon::fake() for testing.
README
A typed PHP client for the ASI Smartlink REST API, built on Saloon. Search products, suppliers, and decorators; pull product detail, charges, and price matrices; resolve media URLs; and drive auto-complete and industry news — all through fluent query builders, immutable DTOs, and typed exceptions. Laravel integration (service provider, facade, publishable config) is included but optional; the client runs equally well in plain PHP.
Warning
Work in progress — pre-release software. This package is under active development and
has not yet reached a stable 1.0 release. The public API, DTO shapes, and method
signatures may change without notice between 0.x releases. Pin an exact version and review the changelog before upgrading.
Not yet recommended for production use.
Installation
You can install the package via composer:
composer require daikazu/asi-smartlink
You can publish the config file with:
php artisan vendor:publish --tag="asi-smartlink-config"
Environment
Add your ASI-issued credentials to .env:
ASI_SMARTLINK_API_URL=https://api.asicentral.com ASI_SMARTLINK_CLIENT_ID=your-client-id ASI_SMARTLINK_SECRET=your-client-secret # optional ASI_SMARTLINK_TIMEOUT=30
Authentication uses ASI's AsiMemberAuth scheme — a single Authorization
header built from the client ID and secret. There is no OAuth/token exchange.
Usage
use Daikazu\AsiSmartlink\Enums\ProductSort; use Daikazu\AsiSmartlink\Facades\AsiSmartlink; $results = AsiSmartlink::products()->search( query: 'mugs', page: 1, resultsPerPage: 25, sort: ProductSort::PriceLowToHigh, dimensionLists: ['supplier', 'category'], ); $results->resultsTotal; // 20552 $results->hasNextPage(); // true foreach ($results->products as $product) { $product->name; // "Purple Plush Santa Hat" $product->number; // "HAT112" $product->supplier?->name; // "Brighter Promotions Inc" $product->prices[0]?->price; // 3.68 }
The same is available via the container, e.g. app(\Daikazu\AsiSmartlink\AsiSmartlink::class)
or by injecting Daikazu\AsiSmartlink\Http\SmartLinkConnector.
Using it without Laravel
Laravel is optional. The package only requires saloonphp/saloon at runtime — the service
provider, facade, and config helpers are conveniences that activate when Laravel is present.
Outside Laravel (plain PHP, Symfony, a CLI script, …) just construct the connector directly:
use Daikazu\AsiSmartlink\Http\SmartLinkConnector; use Daikazu\AsiSmartlink\Support\ProductQuery; $smartlink = new SmartLinkConnector( baseUrl: 'https://api.asicentral.com', clientId: 123456, clientSecret: getenv('ASI_SMARTLINK_SECRET'), ); $results = $smartlink->products()->search( ProductQuery::make('pen')->hasImage()->priceBetween(1, 5) );
The resources (->products(), ->suppliers(), ->decorators(), ->lists(), ->media()),
DTOs, query builders, and typed exceptions all work identically — none of them touch the
framework. Saloon's own MockClient ($connector->withMockClient(...)) handles testing
without the Laravel Saloon::fake() facade.
Available resources
use Daikazu\AsiSmartlink\Facades\AsiSmartlink; use Daikazu\AsiSmartlink\Enums\AutoCompleteDimension; use Daikazu\AsiSmartlink\Enums\MediaSize; // Products AsiSmartlink::products()->search('mugs'); AsiSmartlink::products()->get(4815380); // full detail AsiSmartlink::products()->getByCpn('12345'); // CPN lookup AsiSmartlink::products()->configure(4815380, configId: 1); AsiSmartlink::products()->matrix(4694711); // apparel price grid AsiSmartlink::products()->charges(4815380); // list<ProductCharge> AsiSmartlink::products()->lookupCharges(); // master charge list AsiSmartlink::products()->deletedSince('11/02/2016'); // list<int> // Suppliers & decorators AsiSmartlink::suppliers()->search('promotions'); AsiSmartlink::suppliers()->get(875); AsiSmartlink::decorators()->search('promotions'); AsiSmartlink::decorators()->get(6859235); // Lists (no auth required) AsiSmartlink::lists()->autoComplete(AutoCompleteDimension::Category, 'shirts'); // list<string> AsiSmartlink::lists()->news(); // list<NewsArticle> AsiSmartlink::lists()->newsArticle(46318); // Media (no auth required) AsiSmartlink::media()->url(20542352, MediaSize::Normal); // build URL for <img> AsiSmartlink::media()->get(5072512, MediaSize::Large); // fetch the image bytes
Authentication: most endpoints send the
AsiMemberAuthcredentials automatically. The auto-complete, industry-news, and media endpoints require no authentication, so the package deliberately omits the credentials on those requests.
Note on detail responses:
ProductDetail,SupplierDetail, andDecoratorDetailtype the documented identity/contact fields and expose the complete decoded payload via arawarray (and aget('Key')helper). The SmartLink PDF's "Raw Response" samples are embedded images, so the deeper nested structures carryTODO(confirm)notes pending verification against a live response — see the DTO docblocks.
Filtering product search
The product search q parameter supports dimension and boolean filters. Build them
with ProductQuery instead of hand-writing the string — products()->search() accepts
either a plain keyword string or a ProductQuery:
use Daikazu\AsiSmartlink\Facades\AsiSmartlink; use Daikazu\AsiSmartlink\Support\ProductQuery; $query = ProductQuery::make('pen') ->preferred(1, 2, 3) // preferred supplier ranks 1-5 (or ->preferredAll() for *) ->category('Pens') ->priceBetween(1.00, 3.00) // price:[1 to 3] ->hasImage() // boolean filters: has_image:1 ->isNew(); AsiSmartlink::products()->search($query, resultsPerPage: 25); // q => "pen preferred:1,2,3 category:Pens price:[1 to 3] has_image:1 is_new:1"
Covers all tier-1 dimensions (category, color, size, material, supplier, asi,
priceBetween/costBetween/profitBetween, quantity, productionTime,
supplierCountry, preferred), tier-2 dimensions (shape, theme, tradeName,
supplierState, imprintMethod, lineName, supplierRating), the boolean filters
(hasImage, hasPrice, hasRushService, isNew, isSoldBlank, isMadeInUsa,
isConfirmedProduct, …), and a raw('filter:value') escape hatch. Per the ASI docs,
combine a tier-2 filter with at least one tier-1 selection, and preferred:* slows the
response.
Supplier search has its own builder, SupplierQuery, accepted by suppliers()->search():
use Daikazu\AsiSmartlink\Support\SupplierQuery; // Filter-only search — suppliers in Philadelphia, PA, rated 4+ AsiSmartlink::suppliers()->search( SupplierQuery::make() ->state('PA') // postal abbreviation ->city('Philadelphia') ->supplierRating(4) // minimum rating; emits rating:4 ); // q => "state:PA city:Philadelphia rating:4"
The keyword is AND-ed with every filter. It matches against the supplier name, so pairing a specific keyword with a narrow location easily yields zero results (e.g.
make('promotions')->city('Philadelphia')finds no supplier named "promotions" in Philadelphia). Use the keyword for name searches, or omit it and filter by location.supplierRating(n)matches suppliers ratednor higher.
Supports asi, zip, phone, city, state, email, supplierRating,
canadianFriendly, canadian, and a raw('filter:value') escape hatch.
Decorator search has the equivalent DecoratorQuery (a smaller filter set: asi,
zip, phone, city, state, email), accepted by decorators()->search():
use Daikazu\AsiSmartlink\Support\DecoratorQuery; AsiSmartlink::decorators()->search( DecoratorQuery::make('embroidery')->state('FL')->city('Miami') ); // q => "embroidery state:FL city:Miami"
state:expects the postal abbreviation (FL,PA), even though the address in responses contains the full state name.
Autocomplete → filter
The dimension filters (category, color, supplier, theme, …) match exact values,
so use the auto-complete endpoint to discover valid values as the user types, then feed the
chosen value straight into the query builder. Auto-complete needs no auth and isn't
rate-limited, so it's safe to call on each keystroke.
use Daikazu\AsiSmartlink\Enums\AutoCompleteDimension; use Daikazu\AsiSmartlink\Facades\AsiSmartlink; use Daikazu\AsiSmartlink\Support\ProductQuery; // 1. user types "bl" in the colour field — suggest matching values $suggestions = AsiSmartlink::lists()->autoComplete(AutoCompleteDimension::Color, 'bl'); // ["Black Shades", "Blue Shades", "Bright Blue", ...] // 2. user picks one — feed the exact value into the search filter $results = AsiSmartlink::products()->search( ProductQuery::make('shirt')->color('Blue Shades') );
The AutoCompleteDimension cases line up with the builder methods (Color → color(),
Category → category(), Supplier → supplier(), Theme → theme(), and so on).
Supplier suggestions come back as "Hit Promotional Products (asi/61125)", so you can show
the name and parse the ASI number for an asi: filter.
Error handling
Failed responses (4xx/5xx) throw a typed exception. ASI returns errors as
{"Error": "message"}; that message is surfaced on the exception. All exceptions
extend SmartLinkRequestException, so you can catch one or many:
use Daikazu\AsiSmartlink\Exceptions\AuthenticationException; use Daikazu\AsiSmartlink\Exceptions\ResourceNotFoundException; use Daikazu\AsiSmartlink\Exceptions\RateLimitExceededException; use Daikazu\AsiSmartlink\Exceptions\SmartLinkRequestException; try { $product = AsiSmartlink::products()->get(4815380); } catch (ResourceNotFoundException $e) { // 404 — {"Error":"Product not found"} } catch (AuthenticationException $e) { // 401/403 — bad or missing client_id / client_secret } catch (RateLimitExceededException $e) { // 429 — hourly limit (5,000) exceeded $e->limit(); // 5000 $e->remaining(); // 0 $e->reset(); // X-Rate-Limit-Reset } catch (SmartLinkRequestException $e) { // any other API error (e.g. 5xx) $e->getStatus(); // HTTP status code $e->error(); // ASI "Error" message (null if the body wasn't JSON) $e->getResponse(); // the underlying Saloon response }
| Status | Exception |
|---|---|
| 401 / 403 | AuthenticationException |
| 404 | ResourceNotFoundException |
| 429 | RateLimitExceededException |
| other 4xx/5xx | SmartLinkRequestException |
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.