Repository: doctrine/inflector Branch: 2.1.x Commit: 9065df322d3d Files: 90 Total size: 145.1 KB Directory structure: gitextract_4n9ri0yq/ ├── .doctrine-project.json ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yml │ └── workflows/ │ ├── coding-standards.yml │ ├── composer-lint.yml │ ├── continuous-integration.yml │ ├── release-on-milestone-closed.yml │ ├── static-analysis.yml │ └── website-schema.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── docs/ │ └── en/ │ └── index.rst ├── phpcs.xml.dist ├── phpstan.neon.dist ├── phpunit.xml.dist ├── src/ │ ├── CachedWordInflector.php │ ├── GenericLanguageInflectorFactory.php │ ├── Inflector.php │ ├── InflectorFactory.php │ ├── Language.php │ ├── LanguageInflectorFactory.php │ ├── NoopWordInflector.php │ ├── Rules/ │ │ ├── English/ │ │ │ ├── Inflectible.php │ │ │ ├── InflectorFactory.php │ │ │ ├── Rules.php │ │ │ └── Uninflected.php │ │ ├── Esperanto/ │ │ │ ├── Inflectible.php │ │ │ ├── InflectorFactory.php │ │ │ ├── Rules.php │ │ │ └── Uninflected.php │ │ ├── French/ │ │ │ ├── Inflectible.php │ │ │ ├── InflectorFactory.php │ │ │ ├── Rules.php │ │ │ └── Uninflected.php │ │ ├── Italian/ │ │ │ ├── Inflectible.php │ │ │ ├── InflectorFactory.php │ │ │ ├── Rules.php │ │ │ └── Uninflected.php │ │ ├── NorwegianBokmal/ │ │ │ ├── Inflectible.php │ │ │ ├── InflectorFactory.php │ │ │ ├── Rules.php │ │ │ └── Uninflected.php │ │ ├── Pattern.php │ │ ├── Patterns.php │ │ ├── Portuguese/ │ │ │ ├── Inflectible.php │ │ │ ├── InflectorFactory.php │ │ │ ├── Rules.php │ │ │ └── Uninflected.php │ │ ├── Ruleset.php │ │ ├── Spanish/ │ │ │ ├── Inflectible.php │ │ │ ├── InflectorFactory.php │ │ │ ├── Rules.php │ │ │ └── Uninflected.php │ │ ├── Substitution.php │ │ ├── Substitutions.php │ │ ├── Transformation.php │ │ ├── Transformations.php │ │ ├── Turkish/ │ │ │ ├── Inflectible.php │ │ │ ├── InflectorFactory.php │ │ │ ├── Rules.php │ │ │ └── Uninflected.php │ │ └── Word.php │ ├── RulesetInflector.php │ └── WordInflector.php └── tests/ ├── CachedWordInflectorTest.php ├── InflectorFactoryTest.php ├── InflectorFunctionalTest.php ├── InflectorTest.php ├── NoopWordInflectorTest.php ├── Rules/ │ ├── English/ │ │ └── EnglishFunctionalTest.php │ ├── Esperanto/ │ │ └── EsperantoFunctionalTest.php │ ├── French/ │ │ └── FrenchFunctionalTest.php │ ├── Italian/ │ │ └── ItalianFunctionalTest.php │ ├── LanguageFunctionalTestCase.php │ ├── NorwegianBokmal/ │ │ └── NorwegianBokmalFunctionalTest.php │ ├── PatternTest.php │ ├── PatternsTest.php │ ├── Portuguese/ │ │ └── PortugueseFunctionalTest.php │ ├── RulesetTest.php │ ├── Spanish/ │ │ └── SpanishFunctionalTest.php │ ├── SubstitutionTest.php │ ├── SubstitutionsTest.php │ ├── TransformationTest.php │ ├── TransformationsTest.php │ ├── Turkish/ │ │ └── TurkishFunctionalTest.php │ └── WordTest.php └── RulesetInflectorTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .doctrine-project.json ================================================ { "active": true, "name": "Inflector", "slug": "inflector", "docsSlug": "doctrine-inflector", "versions": [ { "name": "2.2", "branchName": "2.2.x", "slug": "2.2", "upcoming": true }, { "name": "2.1", "branchName": "2.1.x", "slug": "2.1", "current": true }, { "name": "2.0", "slug": "2.0", "maintained": false }, { "name": "1.4", "slug": "1.4", "maintained": false }, { "name": "1.3", "branchName": "1.3.x", "slug": "1.3", "maintained": false }, { "name": "1.2", "branchName": "1.2.x", "slug": "1.2", "maintained": false }, { "name": "1.1", "branchName": "1.1.x", "slug": "1.1", "maintained": false }, { "name": "1.0", "branchName": "1.0.x", "slug": "1.0", "maintained": false } ] } ================================================ FILE: .gitattributes ================================================ /tests export-ignore .* export-ignore phpunit.xml.dist export-ignore phpcs.xml.dist export-ignore composer.lock export-ignore phpstan.neon.dist export-ignore ================================================ FILE: .github/FUNDING.yml ================================================ patreon: phpdoctrine tidelift: packagist/doctrine%2Finflector custom: https://www.doctrine-project.org/sponsorship.html ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" labels: - "CI" ================================================ FILE: .github/workflows/coding-standards.yml ================================================ name: "Coding Standards" on: pull_request: branches: - "*.x" push: branches: - "*.x" jobs: coding-standards: name: "Coding Standards" uses: "doctrine/.github/.github/workflows/coding-standards.yml@13.1.0" ================================================ FILE: .github/workflows/composer-lint.yml ================================================ name: "Composer Lint" on: pull_request: branches: - "*.x" paths: - "composer.json" push: branches: - "*.x" paths: - "composer.json" jobs: composer-lint: name: "Composer Lint" uses: "doctrine/.github/.github/workflows/composer-lint.yml@13.1.0" ================================================ FILE: .github/workflows/continuous-integration.yml ================================================ name: "Continuous Integration" on: pull_request: branches: - "*.x" push: branches: - "*.x" jobs: roave_bc_check: name: "Roave BC Check" runs-on: ubuntu-latest steps: - uses: "actions/checkout@v6" with: fetch-depth: 0 - name: Install PHP with extensions. uses: shivammathur/setup-php@v2 with: php-version: "8.4" - name: Install roave/backward-compatibility-check. run: composer require --dev roave/backward-compatibility-check - name: Run roave/backward-compatibility-check. run: vendor/bin/roave-backward-compatibility-check phpunit: name: "PHPUnit" uses: "doctrine/.github/.github/workflows/continuous-integration.yml@13.1.0" with: php-versions: '["7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"]' secrets: CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" ================================================ FILE: .github/workflows/release-on-milestone-closed.yml ================================================ name: "Automatic Releases" on: milestone: types: - "closed" jobs: release: name: "Git tag, release & create merge-up PR" uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@13.1.0" secrets: GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} ORGANIZATION_ADMIN_TOKEN: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }} SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} ================================================ FILE: .github/workflows/static-analysis.yml ================================================ name: "Static Analysis" on: pull_request: branches: - "*.x" push: branches: - "*.x" jobs: static-analysis-phpstan: name: "Static Analysis" uses: "doctrine/.github/.github/workflows/phpstan.yml@13.1.0" ================================================ FILE: .github/workflows/website-schema.yml ================================================ name: "Website config validation" on: pull_request: branches: - "*.x" paths: - ".doctrine-project.json" - ".github/workflows/website-schema.yml" push: branches: - "*.x" paths: - ".doctrine-project.json" - ".github/workflows/website-schema.yml" jobs: json-validate: name: "Validate JSON schema" uses: "doctrine/.github/.github/workflows/website-schema.yml@13.1.0" ================================================ FILE: .gitignore ================================================ /vendor/ /composer.lock /composer.phar /phpunit.xml /.phpunit.result.cache /phpcs.xml /.phpcs-cache /phpstan.neon ================================================ FILE: LICENSE ================================================ Copyright (c) 2006-2015 Doctrine Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Doctrine Inflector Doctrine Inflector is a small library that can perform string manipulations with regard to uppercase/lowercase and singular/plural forms of words. [![Build Status](https://github.com/doctrine/inflector/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/inflector/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.0.x) [![Code Coverage](https://codecov.io/gh/doctrine/inflector/branch/2.0.x/graph/badge.svg)](https://codecov.io/gh/doctrine/inflector/branch/2.0.x) ================================================ FILE: composer.json ================================================ { "name": "doctrine/inflector", "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", "license": "MIT", "type": "library", "keywords": [ "php", "strings", "words", "manipulation", "inflector", "inflection", "uppercase", "lowercase", "singular", "plural" ], "authors": [ { "name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com" }, { "name": "Roman Borschel", "email": "roman@code-factory.org" }, { "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" }, { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com" } ], "homepage": "https://www.doctrine-project.org/projects/inflector.html", "require": { "php": "^7.2 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^12.0 || ^14.0", "phpstan/phpstan": "^1.12 || ^2.0", "phpstan/phpstan-phpunit": "^1.4 || ^2.0", "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", "phpunit/phpunit": "^8.5 || ^12.2" }, "autoload": { "psr-4": { "Doctrine\\Inflector\\": "src" } }, "autoload-dev": { "psr-4": { "Doctrine\\Tests\\Inflector\\": "tests" } }, "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true }, "sort-packages": true } } ================================================ FILE: docs/en/index.rst ================================================ Introduction ============ The Doctrine Inflector has methods for inflecting text. The features include pluralization, singularization, converting between camelCase and under_score and capitalizing words. Installation ============ You can install the Inflector with composer: .. code-block:: console $ composer require doctrine/inflector Usage ===== Using the inflector is easy, you can create a new ``Doctrine\Inflector\Inflector`` instance by using the ``Doctrine\Inflector\InflectorFactory`` class: .. code-block:: php use Doctrine\Inflector\InflectorFactory; $inflector = InflectorFactory::create()->build(); By default it will create an English inflector. If you want to use another language, just pass the language you want to create an inflector for to the ``createForLanguage()`` method: .. code-block:: php use Doctrine\Inflector\InflectorFactory; use Doctrine\Inflector\Language; $inflector = InflectorFactory::createForLanguage(Language::SPANISH)->build(); The supported languages are as follows: - ``Language::ENGLISH`` - ``Language::ESPERANTO`` - ``Language::FRENCH`` - ``Language::ITALIAN`` - ``Language::NORWEGIAN_BOKMAL`` - ``Language::PORTUGUESE`` - ``Language::SPANISH`` - ``Language::TURKISH`` If you want to manually construct the inflector instead of using a factory, you can do so like this: .. code-block:: php use Doctrine\Inflector\CachedWordInflector; use Doctrine\Inflector\RulesetInflector; use Doctrine\Inflector\Rules\English; $inflector = new Inflector( new CachedWordInflector(new RulesetInflector( English\Rules::getSingularRuleset() )), new CachedWordInflector(new RulesetInflector( English\Rules::getPluralRuleset() )) ); Adding Languages ---------------- If you are interested in adding support for your language, take a look at the other languages defined in the ``Doctrine\Inflector\Rules`` namespace and the tests located in ``Doctrine\Tests\Inflector\Rules``. You can copy one of the languages and update the rules for your language. Once you have done this, send a pull request to the ``doctrine/inflector`` repository with the additions. Custom Setup ============ If you want to setup custom singular and plural rules, you can configure these in the factory: .. code-block:: php use Doctrine\Inflector\InflectorFactory; use Doctrine\Inflector\Rules\Pattern; use Doctrine\Inflector\Rules\Patterns; use Doctrine\Inflector\Rules\Ruleset; use Doctrine\Inflector\Rules\Substitution; use Doctrine\Inflector\Rules\Substitutions; use Doctrine\Inflector\Rules\Transformation; use Doctrine\Inflector\Rules\Transformations; use Doctrine\Inflector\Rules\Word; $inflector = InflectorFactory::create() ->withSingularRules( new Ruleset( new Transformations( new Transformation(new Pattern('/^(bil)er$/i'), '\1'), new Transformation(new Pattern('/^(inflec|contribu)tors$/i'), '\1ta') ), new Patterns(new Pattern('singulars')), new Substitutions(new Substitution(new Word('spins'), new Word('spinor'))) ) ) ->withPluralRules( new Ruleset( new Transformations( new Transformation(new Pattern('^(bil)er$'), '\1'), new Transformation(new Pattern('^(inflec|contribu)tors$'), '\1ta') ), new Patterns(new Pattern('noflect'), new Pattern('abtuse')), new Substitutions( new Substitution(new Word('amaze'), new Word('amazable')), new Substitution(new Word('phone'), new Word('phonezes')) ) ) ) ->build(); No operation inflector ---------------------- The ``Doctrine\Inflector\NoopWordInflector`` may be used to configure an inflector that doesn't perform any operation for pluralization and/or singularization. If will simply return the input as output. This is an implementation of the `Null Object design pattern `_. .. code-block:: php use Doctrine\Inflector\Inflector; use Doctrine\Inflector\NoopWordInflector; $inflector = new Inflector(new NoopWordInflector(), new NoopWordInflector()); Tableize ======== Converts ``ModelName`` to ``model_name``: .. code-block:: php echo $inflector->tableize('ModelName'); // model_name Classify ======== Converts ``model_name`` to ``ModelName``: .. code-block:: php echo $inflector->classify('model_name'); // ModelName Camelize ======== This method uses `Classify`_ and then converts the first character to lowercase: .. code-block:: php echo $inflector->camelize('model_name'); // modelName Capitalize ========== Takes a string and capitalizes all of the words, like PHP's built-in ``ucwords`` function. This extends that behavior, however, by allowing the word delimiters to be configured, rather than only separating on whitespace. Here is an example: .. code-block:: php $string = 'top-o-the-morning to all_of_you!'; echo $inflector->capitalize($string); // Top-O-The-Morning To All_of_you! echo $inflector->capitalize($string, '-_ '); // Top-O-The-Morning To All_Of_You! Pluralize ========= Returns a word in plural form. .. code-block:: php echo $inflector->pluralize('browser'); // browsers Singularize =========== Returns a word in singular form. .. code-block:: php echo $inflector->singularize('browsers'); // browser Urlize ====== Generate a URL friendly string from a string of text: .. code-block:: php echo $inflector->urlize('My first blog post'); // my-first-blog-post Unaccent ======== You can unaccent a string of text using the ``unaccent()`` method: .. code-block:: php echo $inflector->unaccent('año'); // ano Legacy API ========== The API present in Inflector 1.x is still available, but will be deprecated in a future release and dropped for 3.0. Support for languages other than English is available in the 2.0 API only. Acknowledgements ================ The language rules in this library have been adapted from several different sources, including but not limited to: - `Ruby On Rails Inflector `_ - `ICanBoogie Inflector `_ - `CakePHP Inflector `_ ================================================ FILE: phpcs.xml.dist ================================================ src tests ================================================ FILE: phpstan.neon.dist ================================================ includes: - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon parameters: level: 10 paths: - src - tests ================================================ FILE: phpunit.xml.dist ================================================ tests src performance ================================================ FILE: src/CachedWordInflector.php ================================================ wordInflector = $wordInflector; } public function inflect(string $word): string { return $this->cache[$word] ?? $this->cache[$word] = $this->wordInflector->inflect($word); } } ================================================ FILE: src/GenericLanguageInflectorFactory.php ================================================ singularRulesets[] = $this->getSingularRuleset(); $this->pluralRulesets[] = $this->getPluralRuleset(); } final public function build(): Inflector { return new Inflector( new CachedWordInflector(new RulesetInflector( ...$this->singularRulesets )), new CachedWordInflector(new RulesetInflector( ...$this->pluralRulesets )) ); } final public function withSingularRules(?Ruleset $singularRules, bool $reset = false): LanguageInflectorFactory { if ($reset) { $this->singularRulesets = []; } if ($singularRules instanceof Ruleset) { array_unshift($this->singularRulesets, $singularRules); } return $this; } final public function withPluralRules(?Ruleset $pluralRules, bool $reset = false): LanguageInflectorFactory { if ($reset) { $this->pluralRulesets = []; } if ($pluralRules instanceof Ruleset) { array_unshift($this->pluralRulesets, $pluralRules); } return $this; } abstract protected function getSingularRuleset(): Ruleset; abstract protected function getPluralRuleset(): Ruleset; } ================================================ FILE: src/Inflector.php ================================================ 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'Ae', 'Æ' => 'Ae', 'Å' => 'Aa', 'æ' => 'a', 'Ç' => 'C', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'Oe', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'Ue', 'Ý' => 'Y', 'ß' => 'ss', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'ae', 'å' => 'aa', 'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'oe', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'ue', 'ý' => 'y', 'ÿ' => 'y', 'Ā' => 'A', 'ā' => 'a', 'Ă' => 'A', 'ă' => 'a', 'Ą' => 'A', 'ą' => 'a', 'Ć' => 'C', 'ć' => 'c', 'Ĉ' => 'C', 'ĉ' => 'c', 'Ċ' => 'C', 'ċ' => 'c', 'Č' => 'C', 'č' => 'c', 'Ď' => 'D', 'ď' => 'd', 'Đ' => 'D', 'đ' => 'd', 'Ē' => 'E', 'ē' => 'e', 'Ĕ' => 'E', 'ĕ' => 'e', 'Ė' => 'E', 'ė' => 'e', 'Ę' => 'E', 'ę' => 'e', 'Ě' => 'E', 'ě' => 'e', 'Ĝ' => 'G', 'ĝ' => 'g', 'Ğ' => 'G', 'ğ' => 'g', 'Ġ' => 'G', 'ġ' => 'g', 'Ģ' => 'G', 'ģ' => 'g', 'Ĥ' => 'H', 'ĥ' => 'h', 'Ħ' => 'H', 'ħ' => 'h', 'Ĩ' => 'I', 'ĩ' => 'i', 'Ī' => 'I', 'ī' => 'i', 'Ĭ' => 'I', 'ĭ' => 'i', 'Į' => 'I', 'į' => 'i', 'İ' => 'I', 'ı' => 'i', 'IJ' => 'IJ', 'ij' => 'ij', 'Ĵ' => 'J', 'ĵ' => 'j', 'Ķ' => 'K', 'ķ' => 'k', 'ĸ' => 'k', 'Ĺ' => 'L', 'ĺ' => 'l', 'Ļ' => 'L', 'ļ' => 'l', 'Ľ' => 'L', 'ľ' => 'l', 'Ŀ' => 'L', 'ŀ' => 'l', 'Ł' => 'L', 'ł' => 'l', 'Ń' => 'N', 'ń' => 'n', 'Ņ' => 'N', 'ņ' => 'n', 'Ň' => 'N', 'ň' => 'n', 'ʼn' => 'N', 'Ŋ' => 'n', 'ŋ' => 'N', 'Ō' => 'O', 'ō' => 'o', 'Ŏ' => 'O', 'ŏ' => 'o', 'Ő' => 'O', 'ő' => 'o', 'Œ' => 'OE', 'œ' => 'oe', 'Ø' => 'O', 'ø' => 'o', 'Ŕ' => 'R', 'ŕ' => 'r', 'Ŗ' => 'R', 'ŗ' => 'r', 'Ř' => 'R', 'ř' => 'r', 'Ś' => 'S', 'ś' => 's', 'Ŝ' => 'S', 'ŝ' => 's', 'Ş' => 'S', 'ş' => 's', 'Š' => 'S', 'š' => 's', 'Ţ' => 'T', 'ţ' => 't', 'Ť' => 'T', 'ť' => 't', 'Ŧ' => 'T', 'ŧ' => 't', 'Ũ' => 'U', 'ũ' => 'u', 'Ū' => 'U', 'ū' => 'u', 'Ŭ' => 'U', 'ŭ' => 'u', 'Ů' => 'U', 'ů' => 'u', 'Ű' => 'U', 'ű' => 'u', 'Ų' => 'U', 'ų' => 'u', 'Ŵ' => 'W', 'ŵ' => 'w', 'Ŷ' => 'Y', 'ŷ' => 'y', 'Ÿ' => 'Y', 'Ź' => 'Z', 'ź' => 'z', 'Ż' => 'Z', 'ż' => 'z', 'Ž' => 'Z', 'ž' => 'z', 'ſ' => 's', '€' => 'E', '£' => '', ]; /** @var WordInflector */ private $singularizer; /** @var WordInflector */ private $pluralizer; public function __construct(WordInflector $singularizer, WordInflector $pluralizer) { $this->singularizer = $singularizer; $this->pluralizer = $pluralizer; } /** * Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'. */ public function tableize(string $word): string { $tableized = preg_replace('~(?<=\\w)([A-Z])~u', '_$1', $word); if ($tableized === null) { throw new RuntimeException(sprintf( 'preg_replace returned null for value "%s"', $word )); } return mb_strtolower($tableized); } /** * Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'. */ public function classify(string $word): string { return str_replace([' ', '_', '-'], '', ucwords($word, ' _-')); } /** * Camelizes a word. This uses the classify() method and turns the first character to lowercase. */ public function camelize(string $word): string { return lcfirst($this->classify($word)); } /** * Uppercases words with configurable delimiters between words. * * Takes a string and capitalizes all of the words, like PHP's built-in * ucwords function. This extends that behavior, however, by allowing the * word delimiters to be configured, rather than only separating on * whitespace. * * Here is an example: * * capitalize($string); * // Top-O-The-Morning To All_of_you! * * echo $inflector->capitalize($string, '-_ '); * // Top-O-The-Morning To All_Of_You! * ?> * * * @param string $string The string to operate on. * @param string $delimiters A list of word separators. * * @return string The string with all delimiter-separated words capitalized. */ public function capitalize(string $string, string $delimiters = " \n\t\r\0\x0B-"): string { return ucwords($string, $delimiters); } /** * Checks if the given string seems like it has utf8 characters in it. * * @param string $string The string to check for utf8 characters in. */ public function seemsUtf8(string $string): bool { for ($i = 0; $i < strlen($string); $i++) { if (ord($string[$i]) < 0x80) { continue; // 0bbbbbbb } if ((ord($string[$i]) & 0xE0) === 0xC0) { $n = 1; // 110bbbbb } elseif ((ord($string[$i]) & 0xF0) === 0xE0) { $n = 2; // 1110bbbb } elseif ((ord($string[$i]) & 0xF8) === 0xF0) { $n = 3; // 11110bbb } elseif ((ord($string[$i]) & 0xFC) === 0xF8) { $n = 4; // 111110bb } elseif ((ord($string[$i]) & 0xFE) === 0xFC) { $n = 5; // 1111110b } else { return false; // Does not match any model } for ($j = 0; $j < $n; $j++) { // n bytes matching 10bbbbbb follow ? if (++$i === strlen($string) || ((ord($string[$i]) & 0xC0) !== 0x80)) { return false; } } } return true; } /** * Remove any illegal characters, accents, etc. * * @param string $string String to unaccent * * @return string Unaccented string */ public function unaccent(string $string): string { if (preg_match('/[\x80-\xff]/', $string) === false) { return $string; } if ($this->seemsUtf8($string)) { $string = strtr($string, self::ACCENTED_CHARACTERS); } else { $characters = []; // Assume ISO-8859-1 if not UTF-8 $characters['in'] = chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158) . chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194) . chr(195) . chr(196) . chr(197) . chr(199) . chr(200) . chr(201) . chr(202) . chr(203) . chr(204) . chr(205) . chr(206) . chr(207) . chr(209) . chr(210) . chr(211) . chr(212) . chr(213) . chr(214) . chr(216) . chr(217) . chr(218) . chr(219) . chr(220) . chr(221) . chr(224) . chr(225) . chr(226) . chr(227) . chr(228) . chr(229) . chr(231) . chr(232) . chr(233) . chr(234) . chr(235) . chr(236) . chr(237) . chr(238) . chr(239) . chr(241) . chr(242) . chr(243) . chr(244) . chr(245) . chr(246) . chr(248) . chr(249) . chr(250) . chr(251) . chr(252) . chr(253) . chr(255); $characters['out'] = 'EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy'; $string = strtr($string, $characters['in'], $characters['out']); $doubleChars = []; $doubleChars['in'] = [ chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254), ]; $doubleChars['out'] = ['OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th']; $string = str_replace($doubleChars['in'], $doubleChars['out'], $string); } return $string; } /** * Convert any passed string to a url friendly string. * Converts 'My first blog post' to 'my-first-blog-post' * * @param string $string String to urlize. * * @return string Urlized string. */ public function urlize(string $string): string { // Remove all non url friendly characters with the unaccent function $unaccented = $this->unaccent($string); if (function_exists('mb_strtolower')) { $lowered = mb_strtolower($unaccented); } else { $lowered = strtolower($unaccented); } $replacements = [ '/\W/' => ' ', '/([A-Z]+)([A-Z][a-z])/' => '\1_\2', '/([a-z\d])([A-Z])/' => '\1_\2', '/[^A-Z^a-z^0-9^\/]+/' => '-', ]; $urlized = $lowered; foreach ($replacements as $pattern => $replacement) { $replaced = preg_replace($pattern, $replacement, $urlized); if ($replaced === null) { throw new RuntimeException(sprintf( 'preg_replace returned null for value "%s"', $urlized )); } $urlized = $replaced; } return trim($urlized, '-'); } /** * Returns a word in singular form. * * @param string $word The word in plural form. * * @return string The word in singular form. */ public function singularize(string $word): string { return $this->singularizer->inflect($word); } /** * Returns a word in plural form. * * @param string $word The word in singular form. * * @return string The word in plural form. */ public function pluralize(string $word): string { return $this->pluralizer->inflect($word); } } ================================================ FILE: src/InflectorFactory.php ================================================ getFlippedSubstitutions() ); } public static function getPluralRuleset(): Ruleset { return new Ruleset( new Transformations(...Inflectible::getPlural()), new Patterns(...Uninflected::getPlural()), new Substitutions(...Inflectible::getIrregular()) ); } } ================================================ FILE: src/Rules/English/Uninflected.php ================================================ getFlippedSubstitutions() ); } public static function getPluralRuleset(): Ruleset { return new Ruleset( new Transformations(...Inflectible::getPlural()), new Patterns(...Uninflected::getPlural()), new Substitutions(...Inflectible::getIrregular()) ); } } ================================================ FILE: src/Rules/Esperanto/Uninflected.php ================================================ getFlippedSubstitutions() ); } public static function getPluralRuleset(): Ruleset { return new Ruleset( new Transformations(...Inflectible::getPlural()), new Patterns(...Uninflected::getPlural()), new Substitutions(...Inflectible::getIrregular()) ); } } ================================================ FILE: src/Rules/French/Uninflected.php ================================================ */ public static function getSingular(): iterable { // Reverse of -sce → -scia (fasce → fascia) yield new Transformation(new Pattern('([aeiou])sce$'), '\\1scia'); // Reverse of -cie → -cia (farmacia → farmacie) yield new Transformation(new Pattern('cie$'), 'cia'); // Reverse of -gie → -gia (bugia → bugie) yield new Transformation(new Pattern('gie$'), 'gia'); // Reverse of -ce → -cia (arance → arancia) yield new Transformation(new Pattern('([^aeiou])ce$'), '\1cia'); // Reverse of -ge → -gia (valige → valigia) yield new Transformation(new Pattern('([^aeiou])ge$'), '\1gia'); // Reverse of -chi → -co (bachi → baco) yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])chi$'), '\1co'); // Reverse of -ghi → -go (laghi → lago) yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])ghi$'), '\1go'); // Reverse of -ci → -co (medici → medico) yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])ci$'), '\1co'); // Reverse of -gi → -go (psicologi → psicologo) yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])gi$'), '\1go'); // Reverse of -i → -io (zii → zio, negozi → negozio) // This is more complex due to Italian's stress patterns, but we'll handle the basic case yield new Transformation(new Pattern('([^aeiou])i$'), '\1io'); // Handle words that end with -i but should go to -co/-go (amici → amico, not amice) yield new Transformation(new Pattern('([^aeiou])ci$'), '\1co'); yield new Transformation(new Pattern('([^aeiou])gi$'), '\1go'); // Reverse of -a → -e yield new Transformation(new Pattern('e$'), 'a'); // Reverse of -e → -i yield new Transformation(new Pattern('i$'), 'e'); // Reverse of -o → -i yield new Transformation(new Pattern('i$'), 'o'); } /** @return iterable */ public static function getPlural(): iterable { // Words ending in -scia without stress on 'i' become -sce (e.g. fascia → fasce) yield new Transformation(new Pattern('([aeiou])scia$'), '\\1sce'); // Words ending in -cia/gia with stress on 'i' keep the 'i' in plural yield new Transformation(new Pattern('cia$'), 'cie'); // e.g. farmacia → farmacie yield new Transformation(new Pattern('gia$'), 'gie'); // e.g. bugia → bugie // Words ending in -cia/gia without stress on 'i' lose the 'i' in plural yield new Transformation(new Pattern('([^aeiou])cia$'), '\\1ce'); // e.g. arancia → arance yield new Transformation(new Pattern('([^aeiou])gia$'), '\\1ge'); // e.g. valigia → valige // Words ending in -co/-go with stress on 'o' become -chi/-ghi yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])co$'), '\\1chi'); // e.g. baco → bachi yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])go$'), '\\1ghi'); // e.g. lago → laghi // Words ending in -co/-go with stress on the penultimate syllable become -ci/-gi yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])co$'), '\\1ci'); // e.g. medico → medici yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])go$'), '\\1gi'); // e.g. psicologo → psicologi // Words ending in -io with stress on 'i' keep the 'i' in plural yield new Transformation(new Pattern('([^aeiou])io$'), '\\1i'); // e.g. zio → zii // Words ending in -io with stress on 'o' lose the 'i' in plural yield new Transformation(new Pattern('([aeiou])io$'), '\\1i'); // e.g. negozio → negozi // Standard ending rules yield new Transformation(new Pattern('a$'), 'e'); // -a → -e yield new Transformation(new Pattern('e$'), 'i'); // -e → -i yield new Transformation(new Pattern('o$'), 'i'); // -o → -i } /** @return iterable */ public static function getIrregular(): iterable { // Irregular substitutions (singular => plural) $irregulars = [ 'ala' => 'ali', 'albergo' => 'alberghi', 'amica' => 'amiche', 'amico' => 'amici', 'ampio' => 'ampi', 'arancia' => 'arance', 'arma' => 'armi', 'asparago' => 'asparagi', 'banca' => 'banche', 'belga' => 'belgi', 'braccio' => 'braccia', 'budello' => 'budella', 'bue' => 'buoi', 'caccia' => 'cacce', 'calcagno' => 'calcagna', 'camicia' => 'camicie', 'cane' => 'cani', 'capitale' => 'capitali', 'carcere' => 'carceri', 'casa' => 'case', 'cavaliere' => 'cavalieri', 'centinaio' => 'centinaia', 'cerchio' => 'cerchia', 'cervello' => 'cervella', 'chiave' => 'chiavi', 'chirurgo' => 'chirurgi', 'ciglio' => 'ciglia', 'città' => 'città', 'corno' => 'corna', 'corpo' => 'corpi', 'crisi' => 'crisi', 'dente' => 'denti', 'dio' => 'dei', 'dito' => 'dita', 'dottore' => 'dottori', 'fiore' => 'fiori', 'fratello' => 'fratelli', 'fuoco' => 'fuochi', 'gamba' => 'gambe', 'ginocchio' => 'ginocchia', 'gioco' => 'giochi', 'giornale' => 'giornali', 'giraffa' => 'giraffe', 'labbro' => 'labbra', 'lenzuolo' => 'lenzuola', 'libro' => 'libri', 'madre' => 'madri', 'maestro' => 'maestri', 'magico' => 'magici', 'mago' => 'maghi', 'maniaco' => 'maniaci', 'manico' => 'manici', 'mano' => 'mani', 'medico' => 'medici', 'membro' => 'membri', 'metropoli' => 'metropoli', 'migliaio' => 'migliaia', 'miglio' => 'miglia', 'mille' => 'mila', 'mio' => 'miei', 'moglie' => 'mogli', 'mosaico' => 'mosaici', 'muro' => 'muri', 'nemico' => 'nemici', 'nome' => 'nomi', 'occhio' => 'occhi', 'orecchio' => 'orecchi', 'osso' => 'ossa', 'paio' => 'paia', 'pane' => 'pani', 'papa' => 'papi', 'pasta' => 'paste', 'penna' => 'penne', 'pesce' => 'pesci', 'piede' => 'piedi', 'pittore' => 'pittori', 'poeta' => 'poeti', 'porco' => 'porci', 'porto' => 'porti', 'problema' => 'problemi', 'ragazzo' => 'ragazzi', 're' => 're', 'rene' => 'reni', 'riso' => 'risa', 'rosa' => 'rosa', 'sale' => 'sali', 'sarto' => 'sarti', 'scuola' => 'scuole', 'serie' => 'serie', 'serramento' => 'serramenta', 'sorella' => 'sorelle', 'specie' => 'specie', 'staio' => 'staia', 'stazione' => 'stazioni', 'strido' => 'strida', 'strillo' => 'strilla', 'studio' => 'studi', 'suo' => 'suoi', 'superficie' => 'superfici', 'tavolo' => 'tavoli', 'tempio' => 'templi', 'treno' => 'treni', 'tuo' => 'tuoi', 'uomo' => 'uomini', 'uovo' => 'uova', 'urlo' => 'urla', 'valigia' => 'valigie', 'vestigio' => 'vestigia', 'vino' => 'vini', 'viola' => 'viola', 'zio' => 'zii', ]; foreach ($irregulars as $singular => $plural) { yield new Substitution(new Word($singular), new Word($plural)); } } } ================================================ FILE: src/Rules/Italian/InflectorFactory.php ================================================ getFlippedSubstitutions() ); } public static function getPluralRuleset(): Ruleset { return new Ruleset( new Transformations(...Inflectible::getPlural()), new Patterns(...Uninflected::getPlural()), new Substitutions(...Inflectible::getIrregular()) ); } } ================================================ FILE: src/Rules/Italian/Uninflected.php ================================================ */ public static function getSingular(): iterable { yield from self::getDefault(); } /** @return iterable */ public static function getPlural(): iterable { yield from self::getDefault(); } /** @return iterable */ private static function getDefault(): iterable { // Invariable words (same form in singular and plural) $invariables = [ 'alpaca', 'auto', 'bar', 'blu', 'boia', 'boomerang', 'brindisi', 'campus', 'computer', 'crisi', 'crocevia', 'dopocena', 'film', 'foto', 'fuchsia', 'gnu', 'gorilla', 'gru', 'iguana', 'kamikaze', 'karaoke', 'koala', 'lama', 'menu', 'metropoli', 'moto', 'opossum', 'panda', 'quiz', 'radio', 're', 'scacciapensieri', 'serie', 'smartphone', 'sosia', 'sottoscala', 'specie', 'sport', 'tablet', 'taxi', 'vaglia', 'virtù', 'virus', 'yogurt', 'foto', 'fuchsia', ]; foreach ($invariables as $word) { yield new Pattern($word); } } } ================================================ FILE: src/Rules/NorwegianBokmal/Inflectible.php ================================================ getFlippedSubstitutions() ); } public static function getPluralRuleset(): Ruleset { return new Ruleset( new Transformations(...Inflectible::getPlural()), new Patterns(...Uninflected::getPlural()), new Substitutions(...Inflectible::getIrregular()) ); } } ================================================ FILE: src/Rules/NorwegianBokmal/Uninflected.php ================================================ pattern = $pattern; if (isset($this->pattern[0]) && $this->pattern[0] === '/') { $this->regex = $this->pattern; } else { $this->regex = '/' . $this->pattern . '/i'; } } public function getPattern(): string { return $this->pattern; } public function getRegex(): string { return $this->regex; } public function matches(string $word): bool { return preg_match($this->getRegex(), $word) === 1; } } ================================================ FILE: src/Rules/Patterns.php ================================================ getPattern(); }, $patterns); $this->regex = '/^(?:' . implode('|', $patterns) . ')$/i'; } public function matches(string $word): bool { return preg_match($this->regex, $word, $regs) === 1; } } ================================================ FILE: src/Rules/Portuguese/Inflectible.php ================================================ getFlippedSubstitutions() ); } public static function getPluralRuleset(): Ruleset { return new Ruleset( new Transformations(...Inflectible::getPlural()), new Patterns(...Uninflected::getPlural()), new Substitutions(...Inflectible::getIrregular()) ); } } ================================================ FILE: src/Rules/Portuguese/Uninflected.php ================================================ regular = $regular; $this->uninflected = $uninflected; $this->irregular = $irregular; } public function getRegular(): Transformations { return $this->regular; } public function getUninflected(): Patterns { return $this->uninflected; } public function getIrregular(): Substitutions { return $this->irregular; } } ================================================ FILE: src/Rules/Spanish/Inflectible.php ================================================ getFlippedSubstitutions() ); } public static function getPluralRuleset(): Ruleset { return new Ruleset( new Transformations(...Inflectible::getPlural()), new Patterns(...Uninflected::getPlural()), new Substitutions(...Inflectible::getIrregular()) ); } } ================================================ FILE: src/Rules/Spanish/Uninflected.php ================================================ from = $from; $this->to = $to; } public function getFrom(): Word { return $this->from; } public function getTo(): Word { return $this->to; } } ================================================ FILE: src/Rules/Substitutions.php ================================================ substitutions[$substitution->getFrom()->getWord()] = $substitution; } } public function getFlippedSubstitutions(): Substitutions { $substitutions = []; foreach ($this->substitutions as $substitution) { $substitutions[] = new Substitution( $substitution->getTo(), $substitution->getFrom() ); } return new Substitutions(...$substitutions); } public function inflect(string $word): string { $lowerWord = strtolower($word); if (isset($this->substitutions[$lowerWord])) { $firstLetterUppercase = $lowerWord[0] !== $word[0]; $toWord = $this->substitutions[$lowerWord]->getTo()->getWord(); if ($firstLetterUppercase) { return strtoupper($toWord[0]) . substr($toWord, 1); } return $toWord; } return $word; } } ================================================ FILE: src/Rules/Transformation.php ================================================ pattern = $pattern; $this->replacement = $replacement; } public function getPattern(): Pattern { return $this->pattern; } public function getReplacement(): string { return $this->replacement; } public function inflect(string $word): string { return (string) preg_replace($this->pattern->getRegex(), $this->replacement, $word); } } ================================================ FILE: src/Rules/Transformations.php ================================================ transformations = $transformations; } public function inflect(string $word): string { foreach ($this->transformations as $transformation) { if ($transformation->getPattern()->matches($word)) { return $transformation->inflect($word); } } return $word; } } ================================================ FILE: src/Rules/Turkish/Inflectible.php ================================================ getFlippedSubstitutions() ); } public static function getPluralRuleset(): Ruleset { return new Ruleset( new Transformations(...Inflectible::getPlural()), new Patterns(...Uninflected::getPlural()), new Substitutions(...Inflectible::getIrregular()) ); } } ================================================ FILE: src/Rules/Turkish/Uninflected.php ================================================ word = $word; } public function getWord(): string { return $this->word; } } ================================================ FILE: src/RulesetInflector.php ================================================ rulesets = array_merge([$ruleset], $rulesets); } public function inflect(string $word): string { if ($word === '') { return ''; } foreach ($this->rulesets as $ruleset) { if ($ruleset->getUninflected()->matches($word)) { return $word; } $inflected = $ruleset->getIrregular()->inflect($word); if ($inflected !== $word) { return $inflected; } $inflected = $ruleset->getRegular()->inflect($word); if ($inflected !== $word) { return $inflected; } } return $word; } } ================================================ FILE: src/WordInflector.php ================================================ wordInflector->expects(self::once()) ->method('inflect') ->with('in') ->willReturn('out'); self::assertSame('out', $this->cachedWordInflector->inflect('in')); self::assertSame('out', $this->cachedWordInflector->inflect('in')); } protected function setUp(): void { $this->wordInflector = $this->createMock(WordInflector::class); $this->cachedWordInflector = new CachedWordInflector( $this->wordInflector ); } } ================================================ FILE: tests/InflectorFactoryTest.php ================================================ $expectedClass * * @dataProvider provideLanguages */ #[DataProvider('provideLanguages')] public function testCreateForLanguageWithCustomLanguage(string $expectedClass, string $language): void { self::assertInstanceOf($expectedClass, InflectorFactory::createForLanguage($language)); } /** @phpstan-return Generator, Language::*}> */ public static function provideLanguages(): Generator { yield 'English' => [EnglishInflectorFactory::class, Language::ENGLISH]; yield 'Esperanto' => [EsperantoInflectorFactory::class, Language::ESPERANTO]; yield 'French' => [FrenchInflectorFactory::class, Language::FRENCH]; yield 'Norwegian Bokmal' => [NorwegianBokmalInflectorFactory::class, Language::NORWEGIAN_BOKMAL]; yield 'Portuguese' => [PortugueseInflectorFactory::class, Language::PORTUGUESE]; yield 'Spanish' => [SpanishInflectorFactory::class, Language::SPANISH]; yield 'Turkish' => [TurkishInflectorFactory::class, Language::TURKISH]; } public function testCreateForLanguageThrowsInvalidArgumentExceptionForUnsupportedLanguage(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Language "invalid" is not supported.'); InflectorFactory::createForLanguage('invalid')->build(); } } ================================================ FILE: tests/InflectorFunctionalTest.php ================================================ createInflector()->capitalize('top-o-the-morning to all_of_you!') ); } public function testCapitalizeWithCustomDelimiters(): void { self::assertSame( 'Top-O-The-Morning To All_Of_You!', $this->createInflector()->capitalize('top-o-the-morning to all_of_you!', '-_ ') ); } /** @dataProvider dataStringsTableize */ #[DataProvider('dataStringsTableize')] public function testTableize(string $expected, string $word): void { self::assertSame($expected, $this->createInflector()->tableize($word)); } /** * Strings which are used for testTableize. * * @return string[][] */ public static function dataStringsTableize(): array { // In the format array('expected', 'word') return [ ['', ''], ['foo_bar', 'FooBar'], ['f0o_bar', 'F0oBar'], ]; } /** @dataProvider dataStringsClassify */ #[DataProvider('dataStringsClassify')] public function testClassify(string $expected, string $word): void { self::assertSame($expected, $this->createInflector()->classify($word)); } /** * Strings which are used for testClassify. * * @return string[][] */ public static function dataStringsClassify(): array { // In the format array('expected', 'word') return [ ['', ''], ['FooBar', 'foo_bar'], ['FooBar', 'foo bar'], ['F0oBar', 'f0o bar'], ['F0oBar', 'f0o bar'], ['FooBar', 'foo_bar_'], ]; } /** @dataProvider dataStringsCamelize */ #[DataProvider('dataStringsCamelize')] public function testCamelize(string $expected, string $word): void { self::assertSame($expected, $this->createInflector()->camelize($word)); } /** * Strings which are used for testCamelize. * * @return string[][] */ public static function dataStringsCamelize(): array { // In the format array('expected', 'word') return [ ['', ''], ['fooBar', 'foo_bar'], ['fooBar', 'foo bar'], ['f0oBar', 'f0o bar'], ['f0oBar', 'f0o bar'], ]; } private function createInflector(): Inflector { return InflectorFactory::create()->build(); } } ================================================ FILE: tests/InflectorTest.php ================================================ inflector->tableize('ModelName')); self::assertSame('model_name', $this->inflector->tableize('modelName')); self::assertSame('model_name', $this->inflector->tableize('model_name')); } public function testClassify(): void { self::assertSame('ModelName', $this->inflector->classify('model_name')); self::assertSame('ModelName', $this->inflector->classify('modelName')); self::assertSame('ModelName', $this->inflector->classify('ModelName')); } public function testCamelize(): void { self::assertSame('modelName', $this->inflector->camelize('ModelName')); self::assertSame('modelName', $this->inflector->camelize('model_name')); self::assertSame('modelName', $this->inflector->camelize('modelName')); } public function testCapitalize(): void { self::assertSame( 'Top-O-The-Morning To All_of_you!', $this->inflector->capitalize('top-o-the-morning to all_of_you!') ); } public function testSeemsUtf8(): void { self::assertTrue($this->inflector->seemsUtf8('teléfono')); self::assertTrue($this->inflector->seemsUtf8('král')); self::assertTrue($this->inflector->seemsUtf8('telephone')); } public function testUnaccent(): void { self::assertSame('telefono', $this->inflector->unaccent('teléfono')); self::assertSame('telephone', $this->inflector->unaccent('telephone')); } /** @dataProvider dataStringsUrlize */ #[DataProvider('dataStringsUrlize')] public function testUrlize(string $expected, string $string): void { self::assertSame( $expected, $this->inflector->urlize($string) ); } /** * Strings which are used for testUrlize. * * @return string[][] */ public static function dataStringsUrlize(): array { return [ [ 'testing-creating-a-slug-from-a-random-string', 'Testing_Creating a -Slug from a random-string!@#', ], [ 'contesta-el-telefono', 'Contesta el teléfono', ], [ 'den-hund-fuettern', 'den hund füttern', ], [ 'jsem-kral-na-severu', 'Jsem král na severu', ], [ 'test1-test2', 'test1::test2', ], [ 'test1-test2', 'test1$test2', ], [ 'testab-test2', 'TESTAb-test2', ], [ 'ano', 'año', ], ]; } public function testPluralize(): void { $pluralInflector = $this->createMock(WordInflector::class); $pluralInflector->expects(self::once()) ->method('inflect') ->with('in') ->willReturn('out'); $inflector = new Inflector($this->singularInflector, $pluralInflector); self::assertSame('out', $inflector->pluralize('in')); } public function testSingularize(): void { $singularInflector = $this->createMock(WordInflector::class); $inflector = new Inflector($singularInflector, $this->pluralInflector); $singularInflector->expects(self::once()) ->method('inflect') ->with('in') ->willReturn('out'); self::assertSame('out', $inflector->singularize('in')); } protected function setUp(): void { $this->singularInflector = self::createStub(WordInflector::class); $this->pluralInflector = self::createStub(WordInflector::class); $this->inflector = new Inflector($this->singularInflector, $this->pluralInflector); } } ================================================ FILE: tests/NoopWordInflectorTest.php ================================================ inflector->inflect('foo')); self::assertSame('bar', $this->inflector->inflect('bar')); } protected function setUp(): void { $this->inflector = new NoopWordInflector(); } } ================================================ FILE: tests/Rules/English/EnglishFunctionalTest.php ================================================ createInflector()->singularize($singular), sprintf("'%s' should not be singularized to '%s'", $singular, $notEquals) ); } /** * Words without plural test data. * * List of words that don't have a plural form. * * @return string[][] */ public static function dataPluralUninflectedWhenPluralized(): array { return [ ['media'], ]; } /** @dataProvider dataPluralUninflectedWhenPluralized */ #[DataProvider('dataPluralUninflectedWhenPluralized')] public function testPluralsWhenPluralizedShouldBeUninflected(string $plural): void { $pluralized = $this->createInflector()->pluralize($plural); self::assertSame( $plural, $pluralized, sprintf("'%s' should not be pluralized to '%s'", $plural, $pluralized) ); } protected function createInflector(): Inflector { return InflectorFactory::createForLanguage(Language::ENGLISH)->build(); } } ================================================ FILE: tests/Rules/Esperanto/EsperantoFunctionalTest.php ================================================ build(); } } ================================================ FILE: tests/Rules/French/FrenchFunctionalTest.php ================================================ build(); } } ================================================ FILE: tests/Rules/Italian/ItalianFunctionalTest.php ================================================ arms ['ginocchio', 'ginocchia'], // body part ['dito', 'dita'], // more common ['baco', 'bachi'], // more common // Words that change meaning in plural ['membro', 'membri'], // members of an organization ['membrana', 'membrane'], // membranes // Words with identical forms but different genders/meanings ['capitale', 'capitali'], // capital (money) ['capitale', 'capitali'], // capital city (context determines meaning) // Irregular plurals and exceptions ['uomo', 'uomini'], ['dio', 'dei'], ['bue', 'buoi'], // Nouns ending in -o (masculine) ['libro', 'libri'], ['tavolo', 'tavoli'], ['ragazzo', 'ragazzi'], // Nouns ending in -a (feminine) ['casa', 'case'], ['penna', 'penne'], ['amica', 'amiche'], // Nouns ending in -e ['fiore', 'fiori'], ['cane', 'cani'], ['chiave', 'chiavi'], // Nouns ending in -ca/ga ['banca', 'banche'], // Nouns ending in -cia/gia ['arancia', 'arance'], ['valigia', 'valigie'], ['camicia', 'camicie'], ['fascia', 'fasce'], ['farmacia', 'farmacie'], // Nouns ending in -co/go ['gioco', 'giochi'], ['fuoco', 'fuochi'], ['albergo', 'alberghi'], // Words that are the same in both singular and plural ['sosia', 'sosia'], ['vaglia', 'vaglia'], ['gorilla', 'gorilla'], ['yogurt', 'yogurt'], ['boomerang', 'boomerang'], ['kamikaze', 'kamikaze'], ['karaoke', 'karaoke'], ['brindisi', 'brindisi'], ['boia', 'boia'], ]; } protected function createInflector(): Inflector { return InflectorFactory::createForLanguage(Language::ITALIAN)->build(); } } ================================================ FILE: tests/Rules/LanguageFunctionalTestCase.php ================================================ createInflector()->singularize($plural), sprintf("'%s' should be singularized to '%s'", $plural, $singular) ); } /** @dataProvider dataSampleWords */ #[DataProvider('dataSampleWords')] public function testInflectingPlurals(string $singular, string $plural): void { self::assertSame( $plural, $this->createInflector()->pluralize($singular), sprintf("'%s' should be pluralized to '%s'", $singular, $plural) ); } abstract protected function createInflector(): Inflector; } ================================================ FILE: tests/Rules/NorwegianBokmal/NorwegianBokmalFunctionalTest.php ================================================ build(); } } ================================================ FILE: tests/Rules/PatternTest.php ================================================ pattern->getPattern()); } public function testGetRegex(): void { self::assertSame('/test/i', $this->pattern->getRegex()); } public function testPatternWithExplicitRegex(): void { $pattern = new Pattern('/test/'); self::assertSame('/test/', $pattern->getRegex()); } public function testMatches(): void { self::assertTrue($this->pattern->matches('test')); } protected function setUp(): void { $this->pattern = new Pattern('test'); } } ================================================ FILE: tests/Rules/PatternsTest.php ================================================ patterns->matches('test1')); self::assertFalse($this->patterns->matches('test2')); } protected function setUp(): void { $this->patterns = new Patterns(new Pattern('test1')); } } ================================================ FILE: tests/Rules/Portuguese/PortugueseFunctionalTest.php ================================================ build(); } } ================================================ FILE: tests/Rules/RulesetTest.php ================================================ regular, $this->ruleset->getRegular()); } public function testGetUninflected(): void { self::assertSame($this->uninflected, $this->ruleset->getUninflected()); } public function testGetIrregular(): void { self::assertSame($this->irregular, $this->ruleset->getIrregular()); } protected function setUp(): void { $this->regular = new Transformations( new Transformation(new Pattern('test'), 'tests') ); $this->uninflected = new Patterns(new Pattern('uninflected')); $this->irregular = new Substitutions( new Substitution(new Word('test'), new Word('tests')) ); $this->ruleset = new Ruleset( $this->regular, $this->uninflected, $this->irregular ); } } ================================================ FILE: tests/Rules/Spanish/SpanishFunctionalTest.php ================================================ build(); } } ================================================ FILE: tests/Rules/SubstitutionTest.php ================================================ substitution->getFrom()->getWord()); } public function testGetTo(): void { self::assertSame('to', $this->substitution->getTo()->getWord()); } protected function setUp(): void { $this->substitution = new Substitution(new Word('from'), new Word('to')); } } ================================================ FILE: tests/Rules/SubstitutionsTest.php ================================================ irregular->getFlippedSubstitutions(); self::assertSame('spins', $substitutions->inflect('spinor')); } public function testInflect(): void { self::assertSame('spinor', $this->irregular->inflect('spins')); } protected function setUp(): void { $this->substitutions = [ new Substitution(new Word('spins'), new Word('spinor')), ]; $this->irregular = new Substitutions(...$this->substitutions); } } ================================================ FILE: tests/Rules/TransformationTest.php ================================================ transformation->getPattern()->getPattern()); } public function testGetReplacement(): void { self::assertSame('', $this->transformation->getReplacement()); } public function testInflect(): void { self::assertSame('test', $this->transformation->inflect('tests')); } protected function setUp(): void { $this->transformation = new Transformation(new Pattern('s$'), ''); } } ================================================ FILE: tests/Rules/TransformationsTest.php ================================================ transformations->inflect('custom')); } protected function setUp(): void { $this->transformations = new Transformations(new Transformation(new Pattern('/^(custom)$/i'), '\1izables')); } } ================================================ FILE: tests/Rules/Turkish/TurkishFunctionalTest.php ================================================ build(); } } ================================================ FILE: tests/Rules/WordTest.php ================================================ word->getWord()); } protected function setUp(): void { $this->word = new Word('test'); } } ================================================ FILE: tests/RulesetInflectorTest.php ================================================ inflect($input)); } /** @return iterable */ public static function rulesetProvider(): iterable { yield 'irregular uses first match' => [ new Ruleset( new Transformations(), new Patterns(), new Substitutions(new Substitution(new Word('in'), new Word('first'))) ), new Ruleset( new Transformations(), new Patterns(), new Substitutions(new Substitution(new Word('in'), new Word('second'))) ), 'in', 'first', ]; yield 'irregular continues if first ruleset returns original value' => [ new Ruleset( new Transformations(), new Patterns(), new Substitutions() ), new Ruleset( new Transformations(), new Patterns(), new Substitutions(new Substitution(new Word('in'), new Word('second'))) ), 'in', 'second', ]; yield 'uninflected skips on first match' => [ new Ruleset( new Transformations(), new Patterns(new Pattern('in')), new Substitutions() ), new Ruleset( new Transformations(new Transformation(new Pattern('in'), 'should-not-reach')), new Patterns(), new Substitutions() ), 'in', 'in', ]; yield 'irregular is inflected even if later ruleset ignores' => [ new Ruleset( new Transformations(), new Patterns(), new Substitutions(new Substitution(new Word('travel'), new Word('travels'))) ), new Ruleset( new Transformations(), new Patterns(new Pattern('travel')), new Substitutions() ), 'travel', 'travels', ]; yield 'regular uses first match' => [ new Ruleset( new Transformations(new Transformation(new Pattern('in'), 'first')), new Patterns(), new Substitutions() ), new Ruleset( new Transformations(new Transformation(new Pattern('in'), 'second')), new Patterns(), new Substitutions() ), 'in', 'first', ]; yield 'regular continues if first ruleset returns original value' => [ new Ruleset( new Transformations(new Transformation(new Pattern('nomatch'), 'first')), new Patterns(), new Substitutions() ), new Ruleset( new Transformations(new Transformation(new Pattern('in'), 'second')), new Patterns(), new Substitutions() ), 'in', 'second', ]; yield 'returns original value on no matches' => [ new Ruleset( new Transformations(new Transformation(new Pattern('nomatch'), 'replaced')), new Patterns(), new Substitutions() ), new Ruleset( new Transformations(new Transformation(new Pattern('nomatch'), 'replaced')), new Patterns(), new Substitutions() ), 'in', 'in', ]; } }