willwashburn/phpamo
phpamo is a PHP client for the amoCRM API, offering a fluent, object-oriented wrapper around amoCRM's REST endpoints. It handles authentication, leads, contacts, and custom fields, making it simple to pull amoCRM data into Laravel or plain PHP apps.
willwashburn/phpamo is a very small utility: it builds signed Camo proxy URLs. It does not proxy or fetch images itself — you still need a Camo deployment (self-hosted, Heroku, or an internal service) configured with the same shared secret.
Install it alongside your app:
composer require willwashburn/phpamo
The first place to look is the package’s single source class under src/ and the README example. The entire public surface is effectively a constructor plus one URL-building method, so there is very little to learn.
Minimal use:
use Phpamo\Phpamo;
$camo = new Phpamo(
'your-shared-secret', // must match the Camo server's CAMO_KEY
'https://camo.example.com' // your Camo endpoint
);
echo $camo->url('http://cdn.example.com/photo.jpg');
// https://camo.example.com/<hmac-sha1-hex>/687474703a2f2f...
The first real use case is mixed-content cleanup: take user-supplied or third-party http:// image URLs and emit an https:// Camo URL in a Blade view, Markdown renderer, RSS feed, or email template.
Bind it once in the container. Treat the secret and Camo host as configuration, not literals:
// config/services.php
'camo' => [
'key' => env('CAMO_KEY'),
'host' => env('CAMO_HOST', 'https://camo.example.com'),
],
// AppServiceProvider::register()
$this->app->singleton(Phpamo::class, fn () => new Phpamo(
config('services.camo.key'),
config('services.camo.host'),
));
Wrap it in a small app-level helper. Avoid sprinkling the vendor class through views; a CamoUrl service gives you one place to change policy later:
class CamoUrl
{
public function __construct(private Phpamo $phpamo) {}
public function for(?string $url): ?string
{
if (! $url || ! str_starts_with($url, 'http')) {
return $url; // relative paths, data URIs, already handled
}
if (str_starts_with($url, 'https://')) {
return $url; // usually no need to proxy HTTPS sources
}
return $this->phpamo->url($url);
}
}
Use it at render time, not in the database. Generate Camo URLs when composing the response. Signing is a cheap HMAC, so there is no meaningful performance reason to persist derived URLs.
Batch-map content models. For a gallery or feed, map over image fields in a presenter/resource:
$images = $post->images->map(fn ($image) => $camo->url($image->source_url));
Post-process rendered HTML/Markdown when you cannot control the source template. Run the HTML through a DOM pass and rewrite only img[src] values that start with http://. This is a good fit for user-generated Markdown or imported content.
Generate feed and email variants. RSS readers and email clients are the classic mixed-content environments. Emit Camo URLs specifically in those render paths while leaving your main site’s URLs untouched.
Handle multi-tenant secrets at runtime. If each tenant has its own Camo deployment, construct a Phpamo instance per tenant from a repository rather than relying on a single container singleton.
Test the signature, not the string. In a unit test, assert the path segments directly:
$url = 'http://example.com/a.jpg';
$expected = hash_hmac('sha1', $url, 'secret');
$this->assertStringContainsString($expected, $phpamo->url($url));
$this->assertStringEndsWith(bin2hex($url), $phpamo->url($url));
bin2hex), not percent-encoded. Double-encoding produces a valid-looking URL that Camo rejects.hash_hmac('sha1', $sourceUrl, $secret) and bin2hex($sourceUrl) against the two path segments. Nine times out of ten the mismatch is invisible whitespace, an altered query string, or a different secret between app and proxy.data: URIs, and non-HTTP schemes. They cannot be proxied. Also skip https:// sources unless you specifically want Camo’s caching/IP-hiding behavior.http://x/img.png#foo signs the fragment, but the browser never sends it; Camo’s view of the URL may differ depending on how it parses. Strip fragments before signing.http://internal-service/... is an SSRF surface. Enforce an allowlist or block private ranges at the Camo layer; the PHP package will happily sign anything.hash_hmac and bin2hex.How can I help you explore Laravel packages today?