misatotremor/case-bundle
AvroCaseBundle converts strings and arrays of strings between camelCase, kebab-case, PascalCase, Title Case, and underscore_case. Includes Twig filters (camel, kebab, pascal, title, underscore) and a configurable CaseConverter service for Symfony.
Reality check first: this is a Symfony bundle being used from Laravel. Do not try to register Avro\CaseBundle\AvroCaseBundle — Laravel has no config/bundles.php and its Bundle base class is meaningless here. You are adopting exactly one class: Avro\CaseBundle\Util\CaseConverter. Everything Symfony-specific (avro_case.use_twig, the Twig filters, the YAML config) is inert in a Laravel app.
1. Install and inspect the footprint.
composer require misatotremor/case-bundle:^1.3
composer why symfony/http-kernel # confirm what the bundle drags in
composer show misatotremor/case-bundle
Because the bundle class extends Symfony's Bundle, the package almost certainly requires one or more symfony/* packages. Confirm before committing; if your Laravel app doesn't already ship them, you're paying autoload/upgrade cost for a ~100-line utility.
2. Bind it as a singleton. Add to app/Providers/AppServiceProvider.php:
use Avro\CaseBundle\Util\CaseConverter;
public function register(): void
{
$this->app->singleton(CaseConverter::class, fn () => new CaseConverter());
}
Autowiring probably works without this (the class has no required constructor dependencies), but the explicit binding documents intent and gives you a swap point later.
3. Verify behavior in Tinker before writing any code.
php artisan tinker
>>> app(\Avro\CaseBundle\Util\CaseConverter::class)->toCamelCase('user_first-name');
>>> app(\Avro\CaseBundle\Util\CaseConverter::class)->toCamelCase(['user_id', 'created_at']);
Check the array output including keys — the README claims array support but doesn't specify whether keys are preserved or reindexed. That detail determines whether you can use it for field mapping or only for flat lists.
4. First real use case: a single form-request normalizer or an API resource that maps snake_case DB columns to camelCase JSON keys. Keep it to one module until you've compared its output against Illuminate\Support\Str on your real-world strings.
Before you start, do the audit: grep -rn "Str::camel\|Str::snake\|Str::studly\|ucwords\|str_replace('_'" app/. Laravel already covers camel, snake, studly (Pascal), kebab, and title. If your audit shows 90% coverage, you may only need a two-line helper for arrays — not a dependency.
Inject the concrete class, wrap it behind your own interface. The single most valuable thing you can do with a 1-star fork is make it replaceable in one commit.
namespace App\Support\Text;
interface CaseFormatter
{
public function camel(string|array $value): string|array;
public function kebab(string|array $value): string|array;
public function pascal(string|array $value): string|array;
public function title(string|array $value): string|array;
public function underscore(string|array $value): string|array;
}
namespace App\Support\Text;
use Avro\CaseBundle\Util\CaseConverter;
final class AvroCaseFormatter implements CaseFormatter
{
public function __construct(private readonly CaseConverter $converter) {}
public function camel(string|array $value): string|array
{
return $this->converter->toCamelCase($value);
}
// ...one delegating method per format
}
Bind CaseFormatter::class => AvroCaseFormatter::class in the provider. Controllers, jobs, and resources type-hint the interface only. If the package is abandoned, you rewrite one 30-line class.
API resource key mapping. The highest-value pattern is normalizing outgoing payload keys so your DB stays snake_case and your JSON is camelCase:
public function toArray($request): array
{
return collect(parent::toArray($request))
->mapWithKeys(fn ($v, $k) => [$this->formatter->camel($k) => $v])
->all();
}
Do this in a base Resource class rather than per-resource, otherwise the conversion becomes another scattered helper — the exact problem you're trying to solve.
Form request normalization. Convert before validation so first-name, first_name, and firstName all validate against the same rules:
protected function prepareForValidation(): void
{
$this->replace(
collect($this->all())->mapWithKeys(
fn ($v, $k) => [$this->formatter->camel($k) => $v]
)->all()
);
}
prepareForValidation() is the correct hook — it runs before the rules, so required|min:2 applies to the normalized key.
Bulk field mapping for exports/ETL. This is where the array overload genuinely earns its keep, since Str::camel() is scalar-only:
$headers = $this->formatter->camel($query->getConnection()
->getSchemaBuilder()
->getColumnListing('orders'));
// ['id', 'userId', 'createdAt', ...]
Pair it with a hand-written mapping table when the result is user-facing — pascal() on id gives you Id, which is fine for keys but wrong for column headings.
Twig filters: skip them. The camel/kebab/pascal/title/underscore filters only register when Symfony's Twig bundle boots. If you're on a Laravel+Twig setup, register your own filters against the CaseFormatter interface inside a ViewServiceProvider — don't attempt to enable the bundle's extension. In Blade, a directive is simpler:
Blade::directive('camel', fn ($e) => "<?php echo app(\App\Support\Text\CaseFormatter::class)->camel($e); ?>");
Test-first adoption with a golden test. Before replacing anything, write a Pest/PHPUnit data provider asserting the converter's output against Str:: where they overlap, and against hand-written expectations where they don't. This is the pilot; it takes ten minutes and settles every edge-case argument.
The README example is broken. It assigns $this->caseConverter in the constructor but calls $this->converter->toCamelCase(...). Copy-pasting it directly yields Error: Typed property ... must not be accessed before initialization. Trivial, but it's the kind of thing that tells you the docs haven't been walked through recently — verify every signature yourself.
Confirm whether array input preserves keys. This is the single behavior that decides whether the package is worth adopting over Str. Test ['user_id' => 1, 'created_at' => 2] versus ['user_id', 'created_at']. If keys are reindexed, drop it into arr_map_keys() of your own and treat it as a scalar utility only.
Acronyms and mixed-case input are undefined behavior. Str::camel('XMLHttpRequest'), 'API Key', and 'iOS' are exactly where naive regex converters diverge. Run your real payload field names through both the package and Str and diff. If they disagree on anything you actually ship, pick one and lock it behind your CaseFormatter interface with tests, so the choice is explicit and not an accident of which library you happened to call.
Empty strings, nulls, and Unicode. The method signatures are string|array, so passing null will throw a TypeError under strict types. Guard at the boundary: $value ?? '' or a nullable wrapper method. For non-ASCII input, do not assume toTitleCase() is locale-aware — it is almost certainly ASCII ucwords-style logic, so "élan vital" and "iPhone" may not capitalize the way a human would.
Watch the transitive Symfony dependency. Since the bundle class extends Symfony's Bundle, you likely pull symfony/http-kernel and friends. In a Laravel app that's dead weight in composer.lock and one more thing to audit on composer update. If composer why symfony/http-kernel shows no other dependents, weigh that against writing 10 lines yourself.
Config quirk: avro_case.use_twig does nothing for you. Don't create config/avro_case.yaml or a Laravel-side config/avro_case.php expecting it to take effect. The configuration tree is registered by the Symfony extension and only read during bundle boot. Setting it to false "for performance" in a Laravel app is pure cargo cult.
Pin and lock. Use ^1.3 and commit composer.lock. Releases land (1.1.0 → 1.3.0 within the visible window) but with no semantic-versioning safety net from a large user base — a 1.2.x style patch could still change edge-case output, which silently reshapes every API payload downstream. Your golden test is the tripwire.
Debugging workflow. When a conversion looks wrong, isolate it: php artisan tinker with the exact input, then immediately run Str::camel($sameInput) side by side. Nine times out of ten the discrepancy traces to input you didn't expect (double separators, digits adjacent to letters, already-cased input) rather than a library bug.
Know the exit. The extraction path is one file: copy CaseConverter into app/Support/Text/, keep the same CaseFormatter interface, delete the dependency. Because you wrapped it from day one, that's a one-commit change with zero call-site edits. Given the package's traction (1 star, 0 dependents, a fork of an older bundle), plan for that outcome rather than treating it as a failure mode.
When to skip it entirely. If Laravel's Str::camel/snake/studly/kebab/title covers your strings and you only lack array support, write:
function camel_keys(array $data): array
{
return collect($data)->mapWithKeys(fn ($v, $k) => [Str::camel($k) => $v])->all();
}
That is the entire value proposition minus the fork risk. Adopt the package when you specifically need guaranteed identical conversion across a mixed Symfony/Laravel estate, or when a golden test proves its output is the behavior you've already standardized on.
How can I help you explore Laravel packages today?