Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Polyfill Ctype Laravel Package

symfony/polyfill-ctype

Provides ctype_* functions (ctype_alpha, ctype_digit, ctype_alnum, and more) for PHP installations missing the ctype extension, ensuring portable character-type checks across environments. Part of the Symfony Polyfill family.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm you already have it before adding anything. In a typical Laravel app symfony/polyfill-ctype arrives transitively through symfony/http-foundation, symfony/validator, or laravel/framework itself:

composer show symfony/polyfill-ctype
composer why symfony/polyfill-ctype

If composer show returns a version, you're done — nothing to configure, register, or publish. Add it as a direct dependency only when your own application code calls ctype_* and you want that contract declared rather than inherited:

composer require symfony/polyfill-ctype

Where to look first.

  • There is no service provider, no facade, no config file, no Artisan command. Treat it as a runtime shim, not a feature.
  • The per-package README is deliberately a stub — read the main Polyfill README for the family-wide explainer.
  • Open vendor/symfony/polyfill-ctype/bootstrap.php. That file is the whole package: it's the list of guarded functions (ctype_alnum, ctype_alpha, ctype_digit, ctype_lower, ctype_upper, ctype_space, ctype_xdigit, ctype_punct, ctype_graph, ctype_print, ctype_cntrl, ctype_blank) plus the function_exists() checks that keep it dormant when the native extension is present.

First use case — drop the function_exists() guard from a validation rule. The pattern most Laravel developers are carrying around looks like this:

// Before: defensive, noisy, easy to get wrong
if (function_exists('ctype_alnum') && ! ctype_alnum($apiKey)) {
    throw ValidationException::withMessages(['key' => 'Invalid API key format.']);
}

With the polyfill installed you delete the guard and write the intent:

// After
if (! ctype_alnum($apiKey)) {
    throw ValidationException::withMessages(['key' => 'Invalid API key format.']);
}

That is the entire value proposition: ctype_* behaves identically on every host you deploy to. Prove it once by running that snippet in a container built without the ctype extension — that's your "hello world" for this package.

Implementation Patterns

1. Custom validation rules written against native ctype_*. The highest-value pattern. Write the rule for the native function and let the polyfill carry stripped-down hosts:

class AlphanumericKey implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || ! ctype_alnum($value)) {
            $fail('The :attribute must contain only letters and digits.');
        }
    }
}

No extension_loaded('ctype') check, no conditional rule registration, no branching in AppServiceProvider.

2. Form request / middleware input gates. Middleware that filters handles, slugs, or opaque identifiers can call ctype_* unconditionally:

$handle = ltrim((string) $request->input('handle'), '@');

abort_unless(ctype_alnum($handle) && strlen($handle) <= 32, 422);

3. Cheap pre-checks before regex. ctype_* is a fast gate before compiling and running a pattern. Useful in slug pipelines when you build slugs manually rather than via Str::slug:

$isClean = ctype_alnum(str_replace(['-', '_'], '', $slug));

$safe = $isClean ? $slug : Str::slug($slug);

4. Artisan commands and importers. CSV imports, log tailers, and seeders iterate over thousands of rows — exactly where you'd reach for ctype_digit to classify a column:

$valid = array_filter($rows, fn ($row) => ctype_digit((string) $row['id']));

This works identically on a developer laptop, a Forge box, and a minimal Alpine CI image.

5. Declare it explicitly when you author a package. If a Laravel package you maintain calls ctype_* internally, require it directly so a consumer's dependency prune can't remove it from underneath you:

"require": {
    "php": "^8.1",
    "symfony/polyfill-ctype": "^1.34"
}

6. CI matrix as the real contract. A green suite on your laptop exercises native ctype, not the polyfill. Add one job that strips the extension:

- name: Test without ctype
  run: |
    docker run --rm -v $PWD:/app -w /app php:8.2-cli \
      sh -c "php -m | grep -i ctype || echo 'ctype absent'; \
             php -d extension= -r 'require \"vendor/autoload.php\"; var_dump(ctype_alnum(\"abc123\") === true);'"

Without this job, the polyfill branch is dead code in your test suite.

7. Pin via your existing dependency automation. Because the package rarely changes, let Dependabot/Renovate bump it alongside the rest of the Symfony set. Minimum useful pin is ^1.34 — that's the release that aligned E_DEPRECATED behavior with PHP 8.1+.

Gotchas and Tips

It is a silent no-op when the extension is loaded. The bootstrap does function_exists('ctype_*') and skips definition entirely if native exists. Consequences you need to internalize:

  • Unit tests on a healthy host prove nothing about the fallback path.
  • Coverage tools will not tell you the polyfilled branch ran.
  • You must test in an extension-stripped environment to see it work at all.

ASCII-only, always. Both native and polyfilled ctype_* operate on single bytes. ctype_alpha('café') returns false on both — the polyfill preserves the limitation rather than fixing it. The dangerous scenario is a team assuming the polyfill makes international input work. It does not. For multilingual validation use mb_* helpers or Laravel's Str methods; that's a product decision, not a package decision.

Empty strings return false. ctype_digit('') is false, not true. This bites when you're checking optional fields — combine with an explicit emptiness check rather than relying on ctype_* alone.

PHP 8.1+ deprecation noise. The polyfill mirrors native behavior by emitting E_DEPRECATED for non-string arguments. In Laravel this usually surfaces as ctype_digit($request->input('id')) where input() returned an int. Cast at the call site:

ctype_digit((string) $request->input('id'));

Do not blanket-suppress E_DEPRECATED in bootstrap/app.php to quiet this — other Symfony components emit deprecations you actually want to see in Flare/Sentry.

Performance delta in hot loops. Native ctype uses a lookup table; the polyfill loops over bytes — roughly 2–3× slower in community benchmarks. Irrelevant for per-request validation, potentially meaningful in bulk importers or rate limiters. Mitigations, in order of preference:

  1. Enable the native ctype extension on hosts you control (Forge, Vapor, and standard Docker PHP images ship it by default) — the polyfill then steps aside automatically.
  2. Cache validation outcomes for repeated values (API key → user lookup).
  3. Prefer a single preg_match('/^[a-z0-9]+$/i', $v) where predictable perf matters more than readability.

No extension points, by design. No config, no binding, no way to swap the implementation. If you need locale- or Unicode-aware behavior you are not "extending" this package — you're replacing the calls with mb_* or Str helpers. Don't fight it.

Autoloader registration. The package registers via "autoload": { "files": [...] } in its composer.json. A stale autoloader is the usual cause of "ctype functions don't exist" after a composer.lock change:

composer dump-autoload

Debugging checklist for ctype_* "missing" on a host:

  1. composer dump-autoload — files-autoload entries are the mechanism here.
  2. php -r 'var_dump(function_exists("ctype_alnum"));' inside the target container/host, not locally.
  3. php -i | grep ctype — if enabled natively, the polyfill is dormant and you're debugging the native extension, not this package.
  4. Check the resolved version; very old pins predate some function coverage.

Dependency hygiene. Adding it directly when you never call ctype_* in your own source just muddies composer why audits. Rule of thumb: if composer why symfony/polyfill-ctype resolves to a single parent framework/component package, leave it transitive. If you reference ctype_* in app/, declare it and pin ^1.34.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
thomas-0816/doctrine-dbal-duckdb
moyisoft/laravel-admin
oxygen/mod-pages
oxygen/mod-import-export
oxygen/mod-media
ksoft/laravel-contentbuilderjs
ourgoldpackages/exchange
mikkimike/laravel-exchange1c
wandesnet/mercadopago-laravel
tsfcorp/email
larafony/core
laravel-enso/currencies
tartan/laravel-online-payment
wedevbr/celcoin-laravel
surazdott/nepali-date
devsquad-cockpit/laravel
coding-socks/laravel-upload-handler
artisanbarista/laravel-shield
instance-code/repository
flooris/prestashop-models