Skip to content

Case Converter — PHP source

Convert text between UPPER, lower, Title, Sentence, camelCase, PascalCase, snake_case, kebab-case, CONSTANT_CASE and slug. Live, one-click copy.

This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.

<?php
/**
 * case-converter — PHP polyglot showcase port.
 *
 * Pure, deterministic text-case transformations: split any input into word
 * tokens and re-emit it in upper / lower / title / sentence / camel / pascal /
 * snake / kebab / constant / slug case.
 *
 * Ported from src/lib/case.ts (the canonical TypeScript that powers the live
 * Case Converter tool on dev.cosmolabs.org). Functionally equivalent — same
 * inputs produce the same outputs, including edge cases.
 *
 * This is display source — part of CosmoDev's polyglot tool pages (one tool,
 * six language references). stdlib only, no Composer dependencies.
 */

declare(strict_types=1);

namespace CosmoDev\CaseConverter;

/**
 * Pure case conversions exposed as static methods.
 *
 * The static method list mirrors the keys of the TypeScript `caseTransforms`
 * object 1:1; {@see CaseConverter::transforms()} returns the callable registry
 * that corresponds to that object directly.
 */
final class CaseConverter
{
    /**
     * Tokenize a string into lowercase-friendly word units.
     *
     * Mirrors the TypeScript reference:
     *   1. Insert a space at every camelCase boundary
     *      (lowercase-or-digit immediately followed by uppercase).
     *   2. Split on runs of whitespace / underscore / hyphen / dot.
     *   3. Strip remaining non-alphanumerics and drop empty tokens.
     *
     * PCRE is used without the /u flag so \w and the character classes are
     * ASCII-oriented, matching JavaScript's regex behavior.
     *
     * @return list<string>
     */
    public static function words(string $s): array
    {
        // preg_replace can return null only on error; fall back to the raw
        // input so a PCRE failure can never produce a TypeError here.
        $spaced = preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', $s) ?? $s;
        $parts  = preg_split('/[\s_\-.]+/', $spaced) ?: [];

        $out = [];
        foreach ($parts as $part) {
            $cleaned = preg_replace('/[^a-zA-Z0-9]/', '', $part) ?? '';
            if ($cleaned !== '') {
                $out[] = $cleaned;
            }
        }
        return $out;
    }

    /**
     * Capitalize the first character and lowercase the rest.
     *
     * mbstring is used so multi-byte characters are treated as single
     * characters (matching JavaScript's unicode-aware toUpperCase/toLowerCase).
     */
    public static function cap(string $s): string
    {
        if ($s === '') {
            return '';
        }
        return mb_strtoupper(mb_substr($s, 0, 1)) . mb_strtolower(mb_substr($s, 1));
    }

    public static function upper(string $s): string
    {
        return mb_strtoupper($s);
    }

    public static function lower(string $s): string
    {
        return mb_strtolower($s);
    }

    /**
     * Title case: capitalize the first character of each maximal word-run.
     *
     * \w\S* matches a run that starts with a word character; the callback
     * applies {@see cap} to each run. Spacing and punctuation between runs are
     * preserved verbatim.
     */
    public static function title(string $s): string
    {
        return preg_replace_callback(
            '/\w\S*/',
            static fn (array $m): string => self::cap($m[0]),
            $s,
        ) ?? $s;
    }

    /**
     * Sentence case: lowercase + trim the whole string, then uppercase only
     * the first character. Empty input yields empty output.
     */
    public static function sentence(string $s): string
    {
        $t = trim(mb_strtolower($s));
        if ($t === '') {
            return '';
        }
        return mb_strtoupper(mb_substr($t, 0, 1)) . mb_substr($t, 1);
    }

    /** camelCase: first word lowercase, every later word capitalized. */
    public static function camel(string $s): string
    {
        $out = '';
        foreach (self::words($s) as $i => $w) {
            $out .= $i === 0 ? mb_strtolower($w) : self::cap($w);
        }
        return $out;
    }

    /** PascalCase: every word capitalized and concatenated. */
    public static function pascal(string $s): string
    {
        $out = '';
        foreach (self::words($s) as $w) {
            $out .= self::cap($w);
        }
        return $out;
    }

    public static function snake(string $s): string
    {
        return mb_strtolower(implode('_', self::words($s)));
    }

    public static function kebab(string $s): string
    {
        return mb_strtolower(implode('-', self::words($s)));
    }

    public static function constant(string $s): string
    {
        return mb_strtoupper(implode('_', self::words($s)));
    }

    public static function slug(string $s): string
    {
        return mb_strtolower(implode('-', self::words($s)));
    }

    /**
     * The case-transform registry: CaseId => callable.
     *
     * Mirrors the TypeScript `caseTransforms` object 1:1.
     *
     * @return array<string, callable(string): string>
     */
    public static function transforms(): array
    {
        return [
            'upper'    => [self::class, 'upper'],
            'lower'    => [self::class, 'lower'],
            'title'    => [self::class, 'title'],
            'sentence' => [self::class, 'sentence'],
            'camel'    => [self::class, 'camel'],
            'pascal'   => [self::class, 'pascal'],
            'snake'    => [self::class, 'snake'],
            'kebab'    => [self::class, 'kebab'],
            'constant' => [self::class, 'constant'],
            'slug'     => [self::class, 'slug'],
        ];
    }

    /**
     * Apply a named case transform.
     *
     * Convenience dispatcher over {@see transforms()}; the registry itself
     * remains the primary public surface, matching the TypeScript reference.
     *
     * @throws \InvalidArgumentException if $caseId is not a known case.
     */
    public static function convert(string $caseId, string $s): string
    {
        $map = self::transforms();
        if (!isset($map[$caseId])) {
            throw new \InvalidArgumentException("Unknown case: {$caseId}");
        }
        return $map[$caseId]($s);
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →