diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..f4d9307 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,47 @@ +name: PHP Composer + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + php: [8.0, 8.1, 8.2, 8.3] + steps: + - uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: intl #optional + ini-values: "post_max_size=256M" #optional + - name: Check PHP Version + run: php -v + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run lint suite + run: composer run-script lint + + - name: Run test suite + run: vendor/bin/phpunit --coverage-clover=coverage.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab145f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# phpstorm project files +.idea +/.idea +# netbeans project files +nbproject + +# zend studio for eclipse project files +.buildpath +.project +.settings + +# windows thumbnail cache +Thumbs.db + +# composer vendor dir +/vendor + +# composer itself is not needed +composer.phar + +# Mac DS_Store Files +.DS_Store + +# phpunit itself is not needed +phpunit.phar + +tests/_output/* +tests/_support/_generated + +#vagrant folder +/.vagrant +/public_html +/cli +composer.lock \ No newline at end of file diff --git a/.phplint.yml b/.phplint.yml new file mode 100644 index 0000000..83e94a7 --- /dev/null +++ b/.phplint.yml @@ -0,0 +1,9 @@ +path: ./ +jobs: 10 +cache: tests/cache/phplint.cache +extensions: + - php +exclude: + - vendor + - tests +warning: false \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e63bfd4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +@xR2_D2x. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2ad5592 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +Thank you for contributing to this project. + +Please submit pull requests to the master branch only. + +Please run phpunit. + +If you have added something new, create a new unit test. If you have changed something, update all unit tests as necessary. + +We aim to achieve 100% code coverage by tests, including checking for PHP errors and exceptional situations. Therefore, make sure your new or modified tests fully cover all changes you have made. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..38d643a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 vinogradsoft + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0bb4cb1 --- /dev/null +++ b/README.md @@ -0,0 +1,679 @@ +# Navigator + +> Генератор url. Работает с внутренней навигацией приложения и способен генерировать URL-адреса на внешние ресурсы. +> Внутренняя навигация осуществляется на основе именованных определений маршрутов. +> Именованные маршруты дают возможность удобно создавать URL-адреса, не привязываясь к домену или к определению +> маршрута. + +## Общая информация + +Библиотека предназначена для приложений использующих в своем коде маршрутизатор +[FastRoute](https://github.com/nikic/FastRoute#fastroute---fast-request-router-for-php). FastRoute использует +определения маршрутов на основе регулярных выражений, а Navigator создает URL-адреса соответствующие этим определениям +маршрутов. + +Для постоянства кода определениям маршрутов назначаются имена. + +``` + имя определение + маршрута маршрута + /‾‾‾‾‾‾‾\ /‾‾‾‾‾‾‾‾‾‾‾\ +'user/view' => '/user/{id:\d+}', +``` + +Это позволяет манипулировать URL-адресами без внесения изменений в существующий код. Например, для создания URL-адреса +именованного определения `'user' => '/user/{id:\d+}'`, можно использовать следующий код: + +```php +/** relative url */ +echo $urlBuilder->build('/user', ['id' => 100]); # /user/100 + +/** absolute url */ +echo $urlBuilder->build('/user', ['id' => 100], true); # http://mydomain.ru/user/100 +``` + +Если по какой-либо причине потребуется изменить адрес с `/user/100` на `/employee/100`, вам нужно будет просто изменить +настройку маршрута с `'/user/{id:\d+}'` на `'/employee/{id:\d+}'`. После этого код выше будет создавать относительный +URL-адрес `/employee/100` и абсолютный `http://mydomain.ru/employee/100`. + +В объекте класса `Navigator\UrlBuilder` два метода генерации URL: `build` и `buildExternal`. Метод `build` используется +для навигации внутри приложения, а `buildExternal` создает URL для внешних ресурсов и может генерировать только +абсолютные URL-адреса. + +## Установка + +Предпочтительный способ установки - через [composer](http://getcomposer.org/download/). + +Запустите команду + +``` +php composer require vinogradsoft/navigator "^1.0.0" +``` + +Требуется PHP 8.0 или новее. + +## Быстрый старт + +```php + '/user[/{var_name}]', + // many other route definitions in key-value format. + ]) +); + +echo $urlBuilder->build('/user', null, true), '
'; # https://vinograd.soft/user +echo $urlBuilder->build('/user', ['var_name' => 'var_value'], true), '
'; # https://vinograd.soft/user/var_value +echo $urlBuilder->build('user', ['var_name' => 'var_value'], true), '
'; # https://vinograd.soft/user/var_value +echo $urlBuilder->build('/user', ['var_name' => 'var_value']), '
'; # /user/var_value +echo $urlBuilder->build('user', ['var_name' => 'var_value']), '
'; # user/var_value +``` + +## Конструктор + +Конструктор имеет шесть параметров, пожалуй самые главные первые два. +Первым параметром идет `$baseUrl` - в данном примере его значение равно `'https://vinograd.soft'` - это базовый URL, +который используется для генерации абсолютных URL-адресов внутри приложения. + +Второй параметр `$rulesProvider` принимает экземпляр реализации интерфейса `Navigator\RulesProvider`. +В примере используется реализация `Navigator\ArrayRulesProvider`, которая работает с обычным массивом +зарегистрированных маршрутов. По сути это источник именованных определений маршрутов. + +## Параметры метода `build` + +### Параметр `$name` + +Параметром `$name` вы передаете имя маршрута. На основе значения этого параметра система понимает с каким определением +работает и преобразует его в URL-адрес. В примере используется два случая `'user'` и `'/user'`. +Стоит отметить, что символ “/” в начале передаваемого имени никак не влияет на поиск маршрута, он указывает системе, что +данный символ должен быть включен в начало генерируемого URL-адреса. Система распознает этот символ затем +осуществляет поиск маршрута по имени, исключая из него данный символ. После этого “/” используется системой как +индикатор того, что его необходимо включить в начало формируемого относительного URL-адреса. + +### Параметр `$placeholders` + +Может быть `массивом настроек заполнителей` или `null`. + +#### Массив настроек заполнителей + +Из примера, `$urlBuilder` работает с определением маршрута `'/user[/{var_name}]'`, где `var_name` это динамическая +часть, +другими словами переменная. + +В метод `build` мы передали такой массив `['var_name' => 'var_value']`, где `'var_name'` переменная из определения +маршрута, а `'var_value'` ее значение, т.е. то чем `'var_name'` в результате будет заменена, в нашем случае результат +для абсолютного URL-адреса такой `https://vinograd.soft/user/var_value`. + +#### NULL + +NULL используется в случаях генерации статических URL, которым не нужны настройки в директориях пути, такому как это +определение `'user' => '/user'`. + +Пример: + +```php +$urlBuilder = new UrlBuilder( + 'https://vinograd.soft', + new ArrayRulesProvider([ + 'user' => '/user/profile', + ]) +); + +echo $urlBuilder->build('/user', null, true); # https://vinograd.soft/user/profile +``` + +### Параметр `$absolute` + +Этот булевый параметр указывает системе, какой URL-адрес создавать: абсолютный или относительный. Передача `true` +приведет к созданию абсолютного адреса, в противном случае будет создан относительный адрес. Значение по умолчанию +равно `false`. + +## Конфигурация маршрутов + +Имена определений не имеют жесткого формата, поэтому какие давать имена зависит от вас. +Вы можете использовать формат `<контроллер>/<действие>` пример: `post/view`. +Или добавлять в начало название модуля `blog/post/view`. + +Важно запомнить одно, нельзя чтобы первым символом был `/`. Такое имя не правильное `/blog/post/view` в моменте +генерации URL методом `build`, маршрут с таким именем не будет найден, поскольку система будет его искать без +символа `/` +в начале, затем будет выброшено исключение `Navigator\RoutConfigurationException`. + +Пример правильного имени маршрута: + +```php +new ArrayRulesProvider([ + 'blog/post/view' => '/post/{id:\d+}', +]); +``` + +Пример **НЕДОПУСТИМОГО** имени маршрута: + +```php +new ArrayRulesProvider([ + // Not working warrant /blog/post/view + '/blog/post/view' => '/post/{id:\d+}', +]); +``` + +> В именах совсем не обязателен символ `/`, имя может быть и таким `blog.post.view` или каким-то другим не имеющим +> разделителей вовсе. + +Как формировать маршруты вы можете прочитать в документации к библиотеке +[FastRoute](https://github.com/nikic/FastRoute#defining-routes). + +## Заполнители + +### Схема показывает какой заполнитель, за какой участок URL-адреса отвечает + +``` + |------------------------------:src--------------------------------------------------| + | :path ? # | + | /‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\ /‾‾‾‾‾‾‾‾‾\ /‾‾‾‾‾‾\| + |http://grigor:password@vinograd.soft:8080/path/to/resource.json?query=value#fragment| + \__/ \___/ \_____/ \___________/ \__/ \__/ + :scheme :user :password :host :port :suffix +``` + +Не все заполнители доступны для обоих методов генерации. Таблица ниже показывает доступность каждого заполнителя и тип +данных значения. + +| Заполнитель | Доступность для метода `build` | Доступность для метода `buildExternal` | Тип | +|--------------------------------------|:------------------------------:|:--------------------------------------:|:----------------------------------------------------------------------| +| `название переменной из определения` | **ДА** | НЕТ | `string/int`
тип `bool` - для переменных без фигурных скобок`{}`. | +| `:src` | НЕТ | **ДА** | `string` | +| `:scheme` | НЕТ | **ДА** | `string` | +| `:user` | НЕТ | **ДА** | `string` | +| `:password` | НЕТ | **ДА** | `string` | +| `:host` | НЕТ | **ДА** | `string` | +| `:port` | НЕТ | **ДА** | `string` | +| `:path` | НЕТ | **ДА** | `string/array` | +| `:suffix` | НЕТ | **ДА** | `string` | +| `?` | **ДА** | **ДА** | `string/array` | +| `#` | **ДА** | **ДА** | `string` | +| `:strategy` | **ДА** | **ДА** | `string` | +| `:idn` | **ДА** | **ДА** | `bool` | + +## Примеры использования заполнителей + +Для начала сконфигурируем `$urlBuilder` таким образом: + +```php +$urlBuilder = new UrlBuilder( + 'https://vinograd.soft', + new ArrayRulesProvider([ + 'user' => '/user[/{var_name}]', + 'about' => '/about[.html]', + ]) +); +``` + +### **Название переменной из определения** + +> Не обязательные параметры в определениях не обрамленные в фигурные скобки имеют тип заполнителя `bool`. + +#### **Пример 1.** + +Страница "about" имеет в определении суффикс - не обязательный параметр `.html` без фигурных скобок. Требуется +сгенерировать URL с этим суффиксом. + +```php +echo $urlBuilder->build('about', ['.html' => true], true); # https://vinograd.soft/about.html +``` + +#### **Пример 2.** + +Сгенерируйте абсолютный URL-адрес используя необязательный заполнитель для определения `'user' => '/user[/{var_name}]'`. + +```php +echo $urlBuilder->build('/user', ['var_name' => 'my_unique_value'], true); +# https://vinograd.soft/user/my_unique_value +``` + +#### **Пример 3.** + +Требуется сгенерировать относительный URL-адрес с символом `/` в начале для определения +`'user' => '/user[/{var_name}]'`. + +```php +echo $urlBuilder->build('/user', ['var_name' => 'my_unique_value']); # /user/my_unique_value +``` + +#### **Пример 4.** + +Создайте относительный URL-адрес без символа `/` в начале для определения `'user' => '/user[/{var_name}]'`. + +```php +echo $urlBuilder->build('user', ['var_name' => 'my_unique_value']); # user/my_unique_value +``` + +#### **Пример 5.** + +Определение `'user' => '/user[/{var_name}]'` имеет не обязательный параметр `var_name`. Нужно сгенерировать абсолютный +URL-адрес без этого параметра. + +```php +echo $urlBuilder->build('user', null, true); # https://vinograd.soft/user +``` + +--- + +### **:src** + +#### **Пример 1.** + +В приложении есть переменная в которую записан URL-адрес внешнего ресурса. Нужно внести в URL-адрес несколько +изменений: + ++ изменить его схему с `http` на `ftp` ++ добавить имя пользователя `grigor` и пароль `password123` ++ изменить порт на `21`. + +```php +$externalAddress = 'http://another.site:8080/path/to/resource'; +echo $urlBuilder->buildExternal([ + ':src' => $externalAddress, + ':scheme' => 'ftp', + ':user' => 'grigor', + ':password' => 'password123', + ':port' => '21' +]); +# ftp://grigor:password123@another.site:21/path/to/resource +``` + +#### **Пример 2.** + +Добавьте путь `blog/post/41` к URL-адресу главной странице другого сайта `http://another.site`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'http://another.site', + ':path' => ['blog', 'post', 41], +]); +# http://another.site/blog/post/41 +``` + +--- + +### **:scheme** + +#### **Пример 1.** + +Измените схему URL-адреса `http://another.site` с `http` на `https`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'http://another.site', + ':scheme' => 'https', +]); +# https://another.site +``` + +#### **Пример 2.** + +Создайте URL-адрес `https://another.site/path/to/resource` из имеющихся в приложении частей со схемой `https`. + +```php +echo $urlBuilder->buildExternal([ + ':scheme' => 'https', + ':host' => 'another.site', + ':path' => ['path', 'to', 'resource'], +]); +# https://another.site/path/to/resource +``` + +--- + +### **:user** + +#### **Пример.** + +Добавьте имя пользователя `user` для URL-адреса `http://another.site`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'http://another.site', + ':user' => 'user' +]); +# http://user@another.site +``` + +--- + +### **:password** + +> Этот заполнитель работает только в паре с заполнителем `:user`. Часть `:user` либо должна присутствовать в исходном +> URL-адресе передаваемом в заполнителе `:src`, либо должен передаваться в паре с заполнителем `:password`, если `:user` +> в URL его не окажется, будет выброшено исключение `Navigator\BadParameterException`. + +#### **Пример.** + +Добавьте имя пользователя `grigor` и пароль `password123` для URL-адреса `ftp://another.site:21`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'ftp://another.site:21', + ':user' => 'grigor', + ':password' => 'password123' +]); +# ftp://grigor:password123@another.site:21 +``` + +--- + +### **:host** + +> Заполнитель `:host` используется в паре с заполнителем `:scheme`. В случае когда вы не используете +> заполнитель `:src`, хотите создать URL-адрес из частей, то `:host` и `':scheme'` будут обязательными, при отсутствии +> любого из них будет выброшено исключение `Navigator\BadParameterException`. `:host` единственный заполнитель +> который не переопределяет свой участок в URL-адресе переданным заполнителем `:src`. + +#### **Пример.** + +Требуется создать URL-адрес `http://another.site`. + +```php +echo $urlBuilder->buildExternal([ + ':scheme' => 'http', + ':host' => 'another.site' +]); +# http://another.site +``` + +--- + +### **:port** + +#### **Пример.** + +Создайте URL-адрес `http://another.site` с портом `5000`. + +```php +echo $urlBuilder->buildExternal([ + ':scheme' => 'http', + ':host' => 'another.site', + ':port' => '5000' +]); +# http://another.site:5000 +``` + +--- + +### **:path** + +#### **Пример 1.** + +Требуется создать URL-адрес `https://another.site/path/to` используя значение заполнителя с типом `array`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://another.site', + ':path' => ['path', 'to'] +]); +# https://another.site/path/to +``` + +#### **Пример 2.** + +Сгенерируйте URL-адрес `https://another.site/path/to` используя значение заполнителя с типом `string`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://another.site', + ':path' => 'path/to' +]); +# https://another.site/path/to +``` + +--- + +### **:suffix** + +> С суффиксами есть один нюанс, если вы передаете URL-адрес с суффиксом средствами заполнителя `:src` вы не сможете +> его переопределить заполнителем `:suffix`, вы можете его только добавить, поскольку суффиксом может быть любая стока, +> он не анализируется и будет являться частью path. + +#### **Пример 1.** + +Добавьте к URL-адресу `https://another.site/path/to` расширение `.html`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://another.site', + ':path' => 'path/to', + ':suffix' => '.html' +]); +# https://another.site/path/to.html +``` + +#### **Пример 2.** + +Добавьте суффикс `-city` к URL-адресу `https://another.site/path/to?q=value#news`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://another.site/path/to?q=value#news', + ':suffix' => '-city' +]); +# https://another.site/path/to-city?q=value#news +``` + +--- + +### **Заполнитель `?`** + +#### **Пример 1.** + +Добавьте парамер `s` со значением `Hello world` URL-адресу `https://another.site`. + +```php +echo $urlBuilder->buildExternal([':src' => 'https://another.site', '?' => ['s' => 'Hello world']]); +# https://another.site/?s=Hello%20world +``` + +#### **Пример 2.** + +В системе есть подготовленный параметр `s=Hello world` нужно добавить его к URL-адресу `https://another.site`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://another.site', + '?' => 's=Hello world' +]); +# https://another.site/?s=Hello%20world +``` + +#### **Пример 3.** + +Требуется создать URL-адрес для ссылки на результаты поиска другого сайта `https://another.site` со следующими +параметрами: + +```php + [ + 'search' => [ + 'blog' => [ + 'category' => 'news', + 'author' => 'Grigor' + ] + ] + ] +``` + +Код: + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://another.site', + '?' => [ + 'search' => [ + 'blog' => [ + 'category' => 'news', + 'author' => 'Grigor' + ] + ] + ] +]); +# https://another.site/?search%5Bblog%5D%5Bcategory%5D=news&search%5Bblog%5D%5Bauthor%5D=Grigor +``` + +--- + +### **Заполнитель `#`** + +#### **Пример 1.** + +Задача сформировать адрес для ссылки на документацию библиотеки `vinogradsoft/compass`, параграф +`быстрый-старт`. + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://github.com/vinogradsoft/compass', + '#' => 'быстрый-старт' +]); +# https://github.com/vinogradsoft/compass#быстрый-старт +``` + +--- + +### **:strategy** + +> Для использования стратегий создания URL-адресов нужно либо реализовать интерфейс `Compass\UrlStrategy`, +> либо наследоваться от класса `Compass\DefaultUrlStrategy`. Более подробно о принципах работы стратегий можно прочитать +> в документации к библиотеке +> [Compass](https://github.com/vinogradsoft/compass#%D1%81%D1%82%D1%80%D0%B0%D1%82%D0%B5%D0%B3%D0%B8%D0%B8-%D0%BE%D0%B1%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F). + +#### **Пример 1.** + +Задача сделать стратегию для генерации URL-адресов реферальных ссылок с параметром `refid` равному `222`. + +Код стратегии: + +```php +; + +use Compass\DefaultUrlStrategy; +use Compass\Url; + +class ReferralUrlStrategy extends DefaultUrlStrategy +{ + + /** + * @inheritDoc + */ + public function updateQuery(array $items): string + { + $items['refid'] = 222; + return http_build_query($items, '', '&', PHP_QUERY_RFC3986); + } + + /** + * @inheritDoc + */ + public function forceUnlockMethod( + bool &$schemeState, + int &$authoritySate, + int &$relativeUrlState, + array $items, + array $pathItems, + array $queryItems, + bool $updateAbsoluteUrl, + ?string $suffix = null + ): void + { + $relativeUrlState &= ~Url::QUERY_STATE; + } + +} +``` + +Добавим стратегию в конструктор `$urlBuilder`: + +```php +$urlBuilder = new UrlBuilder( + 'https://vinograd.soft', + new ArrayRulesProvider([ + 'user' => '/user[/{var_name}]' + ]), + ['referral' => new ReferralUrlStrategy()] +); +``` + +Генерируем URL-адрес: + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://another.site/path/to/resource', + ':strategy' => 'referral' +]); +# https://another.site/path/to/resource?refid=222 +``` + +#### **Пример 2.** + +Возьмем стратегию из Примера 1 этого параграфа, и сделаем ее стратегией по умолчанию для генерации внешних URL-адресов. + +Заменим в экземпляре класса `Compass\Url` стратегию и передадим его в конструктор нашего `$urlBuilder` пятым +параметром (`$externalUrl`): + +```php +$externalUrl = Url::createBlank(); +$externalUrl->setUpdateStrategy(new ReferralUrlStrategy()); + +$urlBuilder = new UrlBuilder( + 'https://vinograd.soft', + new ArrayRulesProvider([ + 'user' => '/user[/{var_name}]' + ]), + [], + null, + $externalUrl +); +``` + +Генерируем URL-адрес: + +```php +echo $urlBuilder->buildExternal([ + ':src' => 'https://another.site/path/to/resource' +]); +# https://another.site/path/to/resource?refid=222 +``` + +--- + +### **:idn** + +#### **Пример 1.** + +Задача конвертировать URL-адрес `https://россия.рф` в panycode. + +```php +echo $urlBuilder->buildExternal([':src' => 'https://россия.рф', ':idn' => true]); +# https://xn--h1alffa9f.xn--p1ai +``` + +--- + +## Тестировать + +``` php composer tests ``` + +## Содействие + +Пожалуйста, смотрите [ВКЛАД](https://github.com/vinogradsoft/navigator/blob/master/CONTRIBUTING.md) для получения +подробной информации. + +## Лицензия + +Лицензия MIT (MIT). Пожалуйста, смотрите [файл лицензии](https://github.com/vinogradsoft/navigator/blob/master/LICENSE) +для получения дополнительной информации. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4f3cf4c --- /dev/null +++ b/composer.json @@ -0,0 +1,46 @@ +{ + "name": "vinogradsoft/navigator", + "description": "URL generator", + "authors": [ + { + "name": "vinograd", + "email": "cmk.cmyk@mail.ru" + } + ], + "version": "1.0.0", + "minimum-stability": "stable", + "require": { + "php": ">=8.0", + "vinogradsoft/compass": "^1.0", + "nikic/fast-route": "^1.3", + "ext-intl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.6", + "overtrue/phplint": "^2.0" + }, + "autoload": { + "psr-4": { + "Navigator\\": [ + "src/" + ] + } + }, + "type": "context", + "autoload-dev": { + "psr-4": { + "Test\\": "tests/" + } + }, + "repositories": [ + { + "type": "composer", + "url": "https://asset-packagist.org" + } + ], + "scripts": { + "lint": "phplint", + "tests": "php ./vendor/bin/phpunit --colors=always tests", + "coverage": "XDEBUG_MODE=coverage phpunit --colors=always --coverage-html tests/coverage" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9611426 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + tests + + + + + src + + + diff --git a/src/Adapter.php b/src/Adapter.php new file mode 100644 index 0000000..abaa1b9 --- /dev/null +++ b/src/Adapter.php @@ -0,0 +1,24 @@ + $rules + */ + public function __construct(array $rules) + { + $this->rules = $rules; + } + + /** + * @inheritDoc + */ + public function getPattern(string $name): string + { + $name = ltrim($name, "/ \t\n\r\0\x0B"); + if ($name === '') { + throw new RoutConfigurationException('The name cannot be empty.'); + } + return $this->rules[$name] ?? throw new RoutConfigurationException('No route with this name was found.'); + } + +} \ No newline at end of file diff --git a/src/BadParameterException.php b/src/BadParameterException.php new file mode 100644 index 0000000..958a8c4 --- /dev/null +++ b/src/BadParameterException.php @@ -0,0 +1,9 @@ + $segment) { + if ($segment === '' && $index !== 0) { + throw new BadParameterException('Empty optional part'); + } + if ($segment === '' && $index === 0) { + continue; + } + if ($index === 0) { + $data = $this->fillPlaceholders(is_int(strpos($segment, '{')), $segment, $placeholders); + } else { + $data = $this->fillPlaceholders(false, $segment, $placeholders); + } + + if ($data && $emptyCounter === 0) { + + if (str_starts_with($segment, '/')) { + $url .= '/' . $data; + } else { + $url .= $data; + } + + } elseif ($data && $emptyCounter > 0) { + $errorData[] = $data; + } else { + $emptyCounter++; + } + + } + if (!empty($errorData)) { + throw new BadParameterException('Not enough parameters.'); + } + return explode('/', ltrim($url, '/')); + } + + /** + * @param bool $require + * @param string $segment + * @param array $placeholders + * @return string|null + */ + private function fillPlaceholders(bool $require, string $segment, array $placeholders): ?string + { + if (!preg_match_all( + '~' . Std::VARIABLE_REGEX . '~x', $segment, $matches, + PREG_OFFSET_CAPTURE | PREG_SET_ORDER + )) { + if (!str_starts_with($segment, '/')) { + if (!isset($placeholders[$segment])) { + return null; + } + if (!is_bool($placeholders[$segment])) { + throw new BadParameterException('Invalid placeholder type.'); + } + return $placeholders[$segment] ? ltrim($segment, '/') : null; + } + return ltrim($segment, '/'); + } + + $matchesCount = count($matches); + $counter = $matchesCount; + + foreach ($matches as $set) { + $item = $set[1][0]; + $data = $placeholders[$item] ?? null; + if (is_string($data) || is_int($data)) { + + if ( + !preg_match( + '~^' . (isset($set[2]) ? trim($set[2][0]) : Std::DEFAULT_DISPATCH_REGEX) . '$~isu', + (string)$data, + $m, + PREG_OFFSET_CAPTURE + ) + ) { + throw new BadParameterException('Invalid placeholder type.'); + } + + $segment = str_replace($set[0][0], (string)$data, $segment); + + } elseif ($require && $data === null) { + throw new BadParameterException('Not enough parameters.'); + } else { + $counter--; + } + } + if ($matchesCount !== $counter && $counter !== 0) { + throw new BadParameterException('Not enough parameters.'); + } + if (($matchesCount - $counter) === 0) { + return ltrim($segment, '/'); + } + return null; + } + + /** + * @inheritDoc + */ + public function buildStaticPath(string $pattern): string + { + $bracePos = strpos($pattern, '{'); + $squareBracketPos = strpos($pattern, '['); + + if ($bracePos === false && $squareBracketPos === false) { + return ltrim($pattern, '/'); + } elseif ($squareBracketPos !== false && ($squareBracketPos < $bracePos || is_int($squareBracketPos) && is_bool($bracePos))) { + $result = ltrim(substr($pattern, 0, $squareBracketPos), '/'); + if (empty($result)) { + return ''; + } + return $result; + } + + throw new BadParameterException('Bad parameters.'); + } + +} \ No newline at end of file diff --git a/src/RoutConfigurationException.php b/src/RoutConfigurationException.php new file mode 100644 index 0000000..954dfdb --- /dev/null +++ b/src/RoutConfigurationException.php @@ -0,0 +1,9 @@ +baseUrl = $baseUrl; + $this->rulesProvider = $rulesProvider; + $this->strategies = $strategies; + $this->url = $url ?? Url::createBlank(); + $this->url->setSource($this->baseUrl); + $this->adapter = $adapter ?? new FastRouteAdapter(); + $this->externalUrl = $externalUrl; + } + + /** + * @param string $name + * @return UrlStrategy + */ + protected function getStrategy(string $name): UrlStrategy + { + if (array_key_exists($name, $this->strategies)) { + return $this->strategies[$name]; + } + throw new BadParameterException(sprintf('The parameter %s is not registered.', $name)); + } + + /** + * @return string + */ + public function getBaseUrl(): string + { + return $this->baseUrl; + } + + /** + * @param string $name + * @param array|null $placeholders + * @param bool $absolute + * @return string + * @throws RoutConfigurationException + * @throws BadParameterException + */ + public function build( + string $name, + array|null $placeholders = null, + bool $absolute = false + ): string + { + $pathResult = null; + $this->url->clearRelativeUrl(); + if (!$absolute && ($separator = substr($name, 0, 1)) && $separator === '/') { + $pathResult[] = ''; + } + + if (!$placeholders) { + $pattern = $this->rulesProvider->getPattern($name); + $result = $this->adapter->buildStaticPath($pattern); + $this->url->setPath(empty($pathResult) ? ltrim($result, '/') : '/' . ltrim($result, '/')); + } else { + $dynamic = $this->adapter->buildDynamicPath($this->rulesProvider->getPattern($name), $placeholders); + $pathResult = $pathResult ? array_merge($pathResult, $dynamic) : $dynamic; + $this->url->setArrayPath($pathResult); + } + + if ($fragment = $placeholders['#'] ?? null) { + $this->url->setFragment($fragment); + } + + if (!empty($placeholders['?'])) { + $query = $placeholders['?']; + if (is_string($query)) { + $this->url->setQuery($query); + } elseif (is_array($query)) { + $this->url->setArrayQuery($query); + } + } + + $originStrategy = null; + + if ($absolute && isset($placeholders[':idn'])) { + $this->url->setConversionIdnToAscii($placeholders[':idn']); + } elseif ($absolute && !isset($placeholders[':idn'])) { + $this->url->setConversionIdnToAscii(false); + } + + if ($strategyName = $placeholders[':strategy'] ?? null) { + $strategy = $this->getStrategy($strategyName); + $originStrategy = $this->url->getUpdateStrategy(); + $this->url->setUpdateStrategy($strategy); + } + + $this->url->updateSource($absolute); + + if ($absolute) { + $result = $this->url->getSource(); + } else { + $result = $this->url->getRelativeUrl(); + if (empty($result)) { + $result = '/'; + } + } + if (!empty($originStrategy)) { + $this->url->setUpdateStrategy($originStrategy); + } + return $result; + } + + /** + * @param array $placeholders + * @return string + */ + public function buildExternal(array $placeholders): string + { + if (empty($this->externalUrl)) { + $this->externalUrl = Url::createBlank(); + } + try { + if (isset($placeholders[':src'])) { + $this->externalUrl->setSource($placeholders[':src']); + if ($scheme = $placeholders[':scheme'] ?? null) { + $this->externalUrl->setScheme($scheme); + } + if ($user = $placeholders[':user'] ?? null) { + $this->externalUrl->setUser($user); + } + if ($password = $placeholders[':password'] ?? null) { + if (!$this->externalUrl->getUser()) { + throw new BadParameterException('The :user placeholder was not found.'); + } + $this->externalUrl->setPassword($password); + } + } else { + $this->externalUrl->reset(); + $host = $placeholders[':host'] ?? throw new BadParameterException('The :host placeholder was not found.'); + $this->externalUrl->setHost($host); + + $scheme = $placeholders[':scheme'] ?? throw new BadParameterException('The :scheme placeholder was not found.'); + $this->externalUrl->setScheme($scheme); + if ($user = $placeholders[':user'] ?? null) { + $this->externalUrl->setUser($user); + } + if ($password = $placeholders[':password'] ?? null) { + if (!isset($placeholders[':user'])) { + throw new BadParameterException('The :user placeholder was not found.'); + } + $this->externalUrl->setPassword($password); + } + } + + if ($port = $placeholders[':port'] ?? null) { + $this->externalUrl->setPort($port); + } + if ($path = $placeholders[':path'] ?? null) { + if (is_string($path)) { + $this->externalUrl->setPath($path); + } elseif (is_array($path)) { + $this->externalUrl->setArrayPath($path); + } + } + if ($suffix = $placeholders[':suffix'] ?? null) { + $this->externalUrl->setSuffix($suffix); + } + if ($query = $placeholders['?'] ?? null) { + if (is_string($query)) { + $this->externalUrl->setQuery($query); + } elseif (is_array($query)) { + $this->externalUrl->setArrayQuery($query); + } + } + if ($fragment = $placeholders['#'] ?? null) { + $this->externalUrl->setFragment($fragment); + } + + $this->externalUrl->setConversionIdnToAscii($placeholders[':idn'] ?? false); + $originStrategy = null; + if ($strategyName = $placeholders[':strategy'] ?? null) { + $strategy = $this->getStrategy($strategyName); + $originStrategy = $this->externalUrl->getUpdateStrategy(); + $this->externalUrl->setUpdateStrategy($strategy); + } + $this->externalUrl->updateSource(); + $result = $this->externalUrl->getSource(); + if (!empty($originStrategy)) { + $this->externalUrl->setUpdateStrategy($originStrategy); + } + return $result; + } catch (InvalidUrlException $e) { + throw new BadParameterException($e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..4f9ee23 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,2 @@ +/cache/ +/coverage/ \ No newline at end of file diff --git a/tests/Cases/Dummy/ReferralUrlStrategy.php b/tests/Cases/Dummy/ReferralUrlStrategy.php new file mode 100644 index 0000000..9e8dabd --- /dev/null +++ b/tests/Cases/Dummy/ReferralUrlStrategy.php @@ -0,0 +1,37 @@ +rulesProvider = new ArrayRulesProvider( + [ + $this->var1 => $this->value1, + $this->var2 => $this->value2, + $this->var3 => $this->value3, + $this->var4 => $this->value4, + $this->var5 => $this->value5, + ] + ); + } + + /** + * @return void + */ + public function testGetPattern() + { + $result1 = $this->rulesProvider->getPattern($this->var1); + $result2 = $this->rulesProvider->getPattern($this->var2); + $result3 = $this->rulesProvider->getPattern($this->var3); + $result4 = $this->rulesProvider->getPattern($this->var4); + $result5 = $this->rulesProvider->getPattern($this->var5); + self::assertEquals($this->value1, $result1); + self::assertEquals($this->value2, $result2); + self::assertEquals($this->value3, $result3); + self::assertEquals($this->value4, $result4); + self::assertEquals($this->value5, $result5); + + $result1 = $this->rulesProvider->getPattern('/' . $this->var1); + $result2 = $this->rulesProvider->getPattern('/' . $this->var2); + $result3 = $this->rulesProvider->getPattern('/' . $this->var3); + $result4 = $this->rulesProvider->getPattern('/' . $this->var4); + $result5 = $this->rulesProvider->getPattern('/' . $this->var5); + self::assertEquals($this->value1, $result1); + self::assertEquals($this->value2, $result2); + self::assertEquals($this->value3, $result3); + self::assertEquals($this->value4, $result4); + self::assertEquals($this->value5, $result5); + + $result1 = $this->rulesProvider->getPattern('//' . $this->var1); + $result2 = $this->rulesProvider->getPattern('//' . $this->var2); + $result3 = $this->rulesProvider->getPattern('//' . $this->var3); + $result4 = $this->rulesProvider->getPattern('//' . $this->var4); + $result5 = $this->rulesProvider->getPattern('//' . $this->var5); + self::assertEquals($this->value1, $result1); + self::assertEquals($this->value2, $result2); + self::assertEquals($this->value3, $result3); + self::assertEquals($this->value4, $result4); + self::assertEquals($this->value5, $result5); + } + + /** + * @return void + */ + public function testGetPatternException() + { + $this->expectException(RoutConfigurationException::class); + $this->rulesProvider->getPattern('no'); + } + + /** + * @dataProvider getData + */ + public function testConstructAndGetPatternException($name, $badName) + { + $this->expectException(RoutConfigurationException::class); + $this->rulesProvider = new ArrayRulesProvider( + [ + $badName => $this->value1 + ] + ); + $this->rulesProvider->getPattern($name); + } + + /** + * @return array[] + */ + public function getData() + { + return [ + ['/user', '/user'], + [' /user', '/user'], + [' /user', ' /user'], + ['user', '/user'], + ['user', ' /user'], + [' user', ' /user'], + ['user', '/'], + ['/', '/'], + [' /', '/'], + ['/ ', '/'], + ['/', ' /'], + ['/', '/ '], + [' /', '/ '], + [' / ', ' / '], + ['/ ', '/ '], + [' /', ' /'], + ['/', ''], + ['/ ', ' '], + [' / ', ' '], + [' /', ' '], + ['', '/'], + ['', ''], + [' ', ' '], + [' ', ''], + [' ', ''], + ["/ \t\n\r\0\x0B", ''], + ['', ' '] + ]; + } + +} \ No newline at end of file diff --git a/tests/Unit/FastRouteAdapterTest.php b/tests/Unit/FastRouteAdapterTest.php new file mode 100644 index 0000000..eb59806 --- /dev/null +++ b/tests/Unit/FastRouteAdapterTest.php @@ -0,0 +1,187 @@ +fastRouteAdapter = new FastRouteAdapter(); + } + + /** + * @dataProvider getData + */ + public function testBuildDynamicPath($pattern, $placeholders, $expected) + { + $result = $this->fastRouteAdapter->buildDynamicPath($pattern, $placeholders); + self::assertEquals($expected, $result); + } + + /** + * @return array[] + */ + public function getData() + { + return [ + ['/user/{id:\d+}[/{name}]', ['id' => 1], ['user', 1]], + ['/user/{id:\d+}[/{name}]', ['id' => 1, 'name' => 'test'], ['user', 1, 'test']], + ['/user/{id:\d+}', ['id' => 1], ['user', 1]], + ['/user/{id:\d+}/{name}', ['id' => 1, 'name' => 'test'], ['user', '1', 'test']], + ['/user[/{id:\d+}[/{name}]]', ['id' => 1, 'name' => 'test'], ['user', 1, 'test']], + ['/user[/{id:\d+}[/{name}]]', ['id' => 1,], ['user', 1]], + ['/user[/{id:\d+}[/{name}]]', [], ['user']], + ['/user/{name}/{id:[0-9]+}', ['name' => 'test', 'id' => 1,], ['user', 'test', 1]], + ['/user/{name}/{id:[a-z]+}', ['id' => 'aaaaaaa', 'name' => 'test'], ['user', 'test', 'aaaaaaa']], + + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}]]]]', ['id' => 1, 'name' => 'test', 'ids' => 2, 'names' => 'tests'], ['user', '1', 'test', '2', 'tests']], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}]]]]', ['id' => 1, 'name' => 'test', 'ids' => 2,], ['user', '1', 'test', '2']], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}]]]]', ['id' => 1, 'name' => 'test'], ['user', '1', 'test']], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}]]]]', ['id' => 1], ['user', '1']], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}]]]]', [], ['user']], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['key' => 40, 'id' => 1, 'name' => 'test', 'ids' => 2, 'names' => 'tests'], ['user', '1', 'test', '2', 'tests', '40']], + + ['/test/{param}', ['param' => 'paramvalue'], ['test', 'paramvalue']], + ['/test/{param1}/test2/{param2}', ['param1' => 'param1value', 'param2' => 'param2value'], ['test', 'param1value', 'test2', 'param2value']], + ['/test/{param:\d+}', ['param' => 1], ['test', 1]], + ['/test[/{param}]', ['param' => 'paramvalue'], ['test', 'paramvalue']], + ['/test[/{param}]', [], ['test']], + ['/{foo-bar}', ['foo-bar' => 'foo-barvalue'], ['foo-barvalue']], + ['/{_foo:.*}', ['_foo' => '_foovalue'], ['_foovalue']], + ['/te{ param }st', ['param' => 'paramvalue'], ['teparamvaluest']], + ['/test/{ param : \d{1,9} }', ['param' => 1], ['test', '1']], + + ['[test]', ['test' => true], ['test']], + ['[test]', ['test' => false], ['']], + ['/{param}[opt]', ['param' => 'paramvalue', 'opt' => true], ['paramvalueopt']], + ['/{param}[opt]', ['param' => 'paramvalue', 'opt' => false], ['paramvalue']], + ['/{param}[opt]', ['param' => 'paramvalue'], ['paramvalue']], + ['/{param}[opt]', ['param' => 'value', 'opt' => true], ['valueopt']], + ['/{param}[opt]', ['param' => 'value', 'opt' => false], ['value']], + ['/{param}[opt]', ['param' => 'value'], ['value']], + ['/test[opt]', ['opt' => true], ['testopt']], + ['/test[opt]', ['opt' => false], ['test']], + ]; + } + + /** + * @dataProvider getBadData + */ + public function testBuildDynamicPathException($pattern, $placeholders) + { + $this->expectException(BadParameterException::class); + $this->fastRouteAdapter->buildDynamicPath($pattern, $placeholders); + } + + /** + * @return array[] + */ + public function getBadData() + { + return [ + ['/user/{id:\d+}[/{name}]', []], + ['/user/{id:\d+}', ['f' => 1]], + ['/user/{id:\d+}/{test}', ['test' => 1]], + ['/user/{id:\d+}', ['ids' => 1]], + ['/user/{name}', []], + ['/user/{name}', ['ids' => 1]], + ['/user/{name}', ['names' => 'test']], + ['/user/{id:\d+}/{ids:\d+}[/{name}]', ['id' => 1]], + ['/user/{id:\d+}/{ids:\d+}[/{name}]', ['ids' => 1]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['name' => 'test', 'ids' => 2, 'names' => 'tests', 'key' => 40,]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['ids' => 2, 'names' => 'tests', 'key' => 40,]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['names' => 'tests', 'key' => 40,]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['key' => 40,]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]', ['key' => 40,]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['id' => 1, 'ids' => 2, 'names' => 'tests', 'key' => 'key',]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['id' => 1, 'name' => 'test', 'names' => 'tests', 'key' => 'key',]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['id' => 1, 'name' => 'test', 'ids' => 2, 'key' => 'key',]], + ['/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', ['id' => 1, 'name' => 'test', 'key' => 'key',]], + ['/user[/{id:\d+}][/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]', ['id' => 1, 'name' => 'test', 'key' => 'key',]], + ['/user/[/{name}]/{id:\d+}', ['id' => 1]], + ['/user/[/{name}]/{id:\d+}', []], + ['/user/[/{name}]/{id:\d+}', ['id' => 1, 'name' => 'name']], + ['/user/[/{name}]/{id:\d+}', []], + ['/user/[/{name}][/{id:\d+}]', ['id' => 1, 'name' => 'name']], + ['/user/[/{name}][/{id:\d+}]', ['name' => 'name']], + ['/user/[/{name}][/{id:\d+}]', []], + ['/test[opt', []], + ['/test[opt', ['opt' => true,]], + ['/test[opt', ['opt' => false,]], + ['/test[opt[opt2]', ['opt' => true, 'opt2' => true,]], + ['/test[opt[opt2]', ['opt' => true, 'opt2' => false,]], + ['/test[opt[opt2]', ['opt' => false, 'opt2' => true,]], + ['/testopt]', []], + ['/test[]', []], + ['/test[[opt]]', ['opt' => true]], + ['[[test]]', ['test' => true]], + ['/test[/opt]/required', ['opt' => true]], + ]; + } + + /** + * @dataProvider getStaticData + */ + public function testBuildStaticPath($pattern, $expected) + { + $result = $this->fastRouteAdapter->buildStaticPath($pattern); + self::assertEquals($expected, $result); + } + + /** + * @return array + */ + public function getStaticData() + { + return [ + ['[test]', ''], + ['', ''], + ['/test', 'test'], + ['/test/test2', 'test/test2'], + ['/test[opt]', 'test'], + ['/test[/{param}]', 'test'], + ]; + } + + /** + * @dataProvider getBadStaticData + */ + public function testBuildStaticPathException($pattern) + { + $this->expectException(BadParameterException::class); + $this->fastRouteAdapter->buildStaticPath($pattern); + } + + /** + * @return array + */ + public function getBadStaticData() + { + return [ + ['/user/{id:\d+}'], + ['/user/{id:\d+}/{name}'], + ['/user/{name}/{id:[0-9]+}'], + ['/user/{name}/{id:[a-z]+}'], + + ['/test/{param}'], + ['/test/{param1}/test2/{param2}'], + ['/test/{param:\d+}'], + ['/{foo-bar}'], + ['/{_foo:.*}'], + ['/te{ param }st'], + ['/test/{ param : \d{1,9} }'], + ['/{param}[opt]'], + ]; + } + +} \ No newline at end of file diff --git a/tests/Unit/UrlBuilder/BuildExternalTest.php b/tests/Unit/UrlBuilder/BuildExternalTest.php new file mode 100644 index 0000000..ee531b2 --- /dev/null +++ b/tests/Unit/UrlBuilder/BuildExternalTest.php @@ -0,0 +1,258 @@ +urlBuilder = new UrlBuilder( + 'https://vinograd.soft', + new ArrayRulesProvider([]) + ); + } + + /** + * @dataProvider getBadData() + */ + public function testBuildExternalException(array $placeholders) + { + $this->expectException(BadParameterException::class); + $this->urlBuilder->buildExternal($placeholders); + } + + /** + * @return array[] + */ + public function getBadData(): array + { + return [ + [ + [':scheme' => 'http', ':user' => 'user', ':password' => '123'] + ], + [ + [':scheme' => 'http'] + ], + [ + [':host' => 'another.site', ':user' => 'user', ':password' => '123'] + ], + [ + [':host' => 'another.site'] + ], + [ + [':scheme' => 'http', ':host' => 'another.site', ':password' => '123'] + ], + [ + [':src' => 'https://another.site', ':password' => '123'] + ], + [ + [':src' => 'another.site'] + ], + [ + [':src' => 'https://'] + ], + [ + [':src' => 'test'] + ], + ]; + } + + /** + * @dataProvider getData() + */ + public function testBuildExternal(array $placeholders, string $expected) + { + $result = $this->urlBuilder->buildExternal($placeholders); + self::assertEquals($expected, $result); + } + + /** + * @return void + */ + public function testBuildExternalDouble() + { + $result = $this->urlBuilder->buildExternal([':src' => 'https://another.site', ':user' => 'user', ':password' => '123']); + self::assertEquals('https://user:123@another.site', $result); + $result = $this->urlBuilder->buildExternal([':src' => 'https://another.site', ':scheme' => 'https']); + self::assertEquals('https://another.site', $result); + + $result = $this->urlBuilder->buildExternal([':scheme' => 'https', ':host' => 'another.site', ':user' => 'user', ':password' => '123', ':path' => ['path', 'to'], '?' => 'key=value', '#' => 'fragment']); + self::assertEquals('https://user:123@another.site/path/to?key=value#fragment', $result); + $result = $this->urlBuilder->buildExternal([':scheme' => 'https', ':host' => 'another.site']); + self::assertEquals('https://another.site', $result); + } + + /** + * @return array + */ + public function getData() + { + return [ + [ + [':src' => 'https://another.site', ':user' => 'user', ':password' => '123'], + 'https://user:123@another.site' + ], + [ + [':src' => 'https://another.site', ':scheme' => 'ftp'], + 'ftp://another.site' + ], + [ + [':scheme' => 'ftp', ':host' => 'another.site', ':user' => 'user', ':password' => '123'], + 'ftp://user:123@another.site' + ], + [ + [':src' => 'https://another.site', ':port' => '8080'], + 'https://another.site:8080' + ], + [ + [':src' => 'https://another.site', ':path' => 'path/to'], + 'https://another.site/path/to' + ], + [ + [':src' => 'https://another.site', ':path' => ['path', 'to']], + 'https://another.site/path/to' + ], + [ + [':src' => 'https://another.site', ':path' => ['path', 'to'], ':suffix' => '.html'], + 'https://another.site/path/to.html' + ], + [ + [':src' => 'https://another.site', ':path' => ['path', 'to'], ':suffix' => '-city'], + 'https://another.site/path/to-city' + ], + [ + [':src' => 'https://another.site', ':suffix' => '-city'], + 'https://another.site' + ], + [ + [':src' => 'https://another.site', '?' => 'key=value'], + 'https://another.site/?key=value' + ], + [ + [':src' => 'https://another.site', '?' => ['key' => 'value']], + 'https://another.site/?key=value' + ], + [ + [':src' => 'https://another.site', '#' => 'fragment'], + 'https://another.site/#fragment' + ], + [ + [':src' => 'https://another.site', ':user' => 'user', ':password' => '123', ':path' => ['path', 'to'], '?' => ['key' => 'value'], '#' => 'fragment'], + 'https://user:123@another.site/path/to?key=value#fragment' + ], + [ + [':src' => 'https://another.site', ':user' => 'user', ':password' => '123', ':path' => ['path', 'to'], '?' => 'key=value', '#' => 'fragment'], + 'https://user:123@another.site/path/to?key=value#fragment' + ], + [ + [':src' => 'https://another.site', ':user' => 'user', ':password' => '123', ':path' => 'path/to', '?' => 'key=value', '#' => 'fragment'], + 'https://user:123@another.site/path/to?key=value#fragment' + ], + [ + [':src' => 'https://another.site', ':user' => 'user', ':password' => '123', ':path' => 'path/to', '?' => ['key' => 'value'], '#' => 'fragment'], + 'https://user:123@another.site/path/to?key=value#fragment' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':user' => 'user', ':password' => '123', ':path' => ['path', 'to'], '?' => ['key' => 'value'], '#' => 'fragment'], + 'https://user:123@another.site/path/to?key=value#fragment' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':user' => 'user', ':password' => '123', ':path' => ['path', 'to'], '?' => 'key=value', '#' => 'fragment'], + 'https://user:123@another.site/path/to?key=value#fragment' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':user' => 'user', ':password' => '123', ':path' => 'path/to', '?' => 'key=value', '#' => 'fragment'], + 'https://user:123@another.site/path/to?key=value#fragment' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':user' => 'user', ':password' => '123', ':path' => 'path/to', '?' => ['key' => 'value'], '#' => 'fragment'], + 'https://user:123@another.site/path/to?key=value#fragment' + ], + + [ + [':scheme' => 'https', ':host' => 'another.site', ':user' => 'user', ':password' => '123'], + 'https://user:123@another.site' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':user' => 'user', ':password' => '123'], + 'https://user:123@another.site' + ], + [ + ['https', ':host' => 'another.site', ':scheme' => 'ftp'], + 'ftp://another.site' + ], + [ + [':scheme' => 'ftp', ':host' => 'another.site', ':user' => 'user', ':password' => '123'], + 'ftp://user:123@another.site' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':port' => '8080'], + 'https://another.site:8080' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':path' => 'path/to'], + 'https://another.site/path/to' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':path' => ['path', 'to']], + 'https://another.site/path/to' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':path' => ['path', 'to'], ':suffix' => '.html'], + 'https://another.site/path/to.html' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':path' => ['path', 'to'], ':suffix' => '-city'], + 'https://another.site/path/to-city' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', ':suffix' => '-city'], + 'https://another.site' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', '?' => 'key=value'], + 'https://another.site/?key=value' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', '?' => ['key' => 'value']], + 'https://another.site/?key=value' + ], + [ + [':scheme' => 'https', ':host' => 'another.site', '#' => 'fragment'], + 'https://another.site/#fragment' + ], + ]; + } + + /** + * @return void + */ + public function testBuildExternalWithStrategy() + { + $urlBuilder = new UrlBuilder( + 'https://another.site', + new ArrayRulesProvider([]), + ['referral' => new ReferralUrlStrategy()] + ); + $result = $urlBuilder->buildExternal([':src' => 'https://another.site', ':strategy' => 'referral']); + self::assertEquals('https://another.site/?refid=222', $result); + $result = $urlBuilder->buildExternal([':src' => 'https://another.site/path/to/resource', ':strategy' => 'referral']); + self::assertEquals('https://another.site/path/to/resource?refid=222', $result); + } + +} \ No newline at end of file diff --git a/tests/Unit/UrlBuilder/BuildTest.php b/tests/Unit/UrlBuilder/BuildTest.php new file mode 100644 index 0000000..c666d22 --- /dev/null +++ b/tests/Unit/UrlBuilder/BuildTest.php @@ -0,0 +1,483 @@ +urlBuilder = new UrlBuilder( + self::BASE_URL, + new ArrayRulesProvider([ + 'user1' => '/user/{id:\d+}[/{name}]', + 'user2' => '/user[/{id:\d+}[/{name}]]', + 'user3' => '/user/{id:\d+}/{name}', + 'user4' => '/user/{name}/{id:[0-9]+}', + 'user5' => '/user/{name}/{id:[a-z]+}', + 'user6' => '/user/{id:\d+}', + 'user7' => '/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}]]]]', + 'user8' => '/user[/{id:\d+}[/{name}[/{ids:\d+}[/{names}/{key:\d+}]]]]', + + 'test1' => '/test/{param}', + 'test2' => '/test[/{param}]', + 'test3' => '/test/{param:\d+}', + 'test4' => '/test/{param1}/test2/{param2}', + 'test5' => '/test/{ param : \d{1,4} }', + 'test6' => '[test]', + 'test7' => '/test[opt]', + + 'foo-bar' => '/{foo-bar}', + '_foo' => '/{_foo:.*}', + 'te' => '/te{ param }st', + 'param/opt' => '/{param}[opt]', + + 'static1' => '[test]', + 'static2' => '', + 'static3' => '/test', + 'static4' => '/test/test2', + 'static5' => '/test[opt]', + 'static6' => '/test[/{param}]', + ]) + ); + } + + /** + * @return void + */ + public function testGetBaseUrl() + { + $baseUrl = $this->urlBuilder->getBaseUrl(); + self::assertEquals(self::BASE_URL, $baseUrl); + } + + /** + * @dataProvider getData + */ + public function testBuild($routName, $placeholders, $expected, $absolute, $slash) + { + $result = $this->urlBuilder->build($slash ? '/' . $routName : $routName, $placeholders, $absolute); + if (!$absolute) { + if ($expected === '') { + self::assertEquals('/', $result); + } else { + self::assertEquals($slash ? '/' . $expected : $expected, $result); + } + } else { + if ($expected === '') { + self::assertEquals(self::BASE_URL, $result); + } else { + self::assertEquals(self::BASE_URL . '/' . $expected, $result); + } + } + } + + /** + * @return array + */ + public function getData() + { + return array_merge( + $this->getTestData(true), + $this->getTestData(true, true), + $this->getTestData(false), + $this->getTestData(false, true), + ); + } + + /** + * @param bool $absolute + * @param bool $slash + * @return array[] + */ + protected function getTestData(bool $absolute, bool $slash = false): array + { + return [ + ['user1', ['id' => 1], 'user/1', $absolute, $slash], + ['user1', ['id' => 1, 'name' => 'test'], 'user/1/test', $absolute, $slash], + ['user6', ['id' => 1], 'user/1', $absolute, $slash], + ['user3', ['id' => 1, 'name' => 'test'], 'user/1/test', $absolute, $slash], + ['user2', ['id' => 1, 'name' => 'test'], 'user/1/test', $absolute, $slash], + ['user2', ['id' => 1,], 'user/1', $absolute, $slash], + ['user2', [], 'user', $absolute, $slash], + ['user4', ['name' => 'test', 'id' => 1,], 'user/test/1', $absolute, $slash], + ['user5', ['id' => 'aaaaaaa', 'name' => 'test'], 'user/test/aaaaaaa', $absolute, $slash], + ['user7', ['id' => 1, 'name' => 'test', 'ids' => 2, 'names' => 'tests'], 'user/1/test/2/tests', $absolute, $slash], + ['user7', ['id' => 1, 'name' => 'test', 'ids' => 2,], 'user/1/test/2', $absolute, $slash], + ['user7', ['id' => 1, 'name' => 'test'], 'user/1/test', $absolute, $slash], + ['user7', ['id' => 1], 'user/1', $absolute, $slash], + ['user7', [], 'user', $absolute, $slash], + ['user8', ['key' => 40, 'id' => 1, 'name' => 'test', 'ids' => 2, 'names' => 'tests'], 'user/1/test/2/tests/40', $absolute, $slash], + + ['test1', ['param' => 'paramvalue'], 'test/paramvalue', $absolute, $slash], + ['test4', ['param1' => 'param1value', 'param2' => 'param2value'], 'test/param1value/test2/param2value', $absolute, $slash], + ['test3', ['param' => 1], 'test/1', $absolute, $slash], + ['test2', ['param' => 'paramvalue'], 'test/paramvalue', $absolute, $slash], + ['test2', [], 'test', $absolute, $slash], + ['foo-bar', ['foo-bar' => 'foo-barvalue'], 'foo-barvalue', $absolute, $slash], + ['_foo', ['_foo' => '_foovalue'], '_foovalue', $absolute, $slash], + ['te', ['param' => 'paramvalue'], 'teparamvaluest', $absolute, $slash], + ['test5', ['param' => 1], 'test/1', $absolute, $slash], + + ['test6', ['test' => true], 'test', $absolute, $slash], + ['test6', ['test' => false], '', $absolute, $slash], + + ['param/opt', ['param' => 'paramvalue', 'opt' => true], 'paramvalueopt', $absolute, $slash], + ['param/opt', ['param' => 'paramvalue', 'opt' => false], 'paramvalue', $absolute, $slash], + ['param/opt', ['param' => 'paramvalue'], 'paramvalue', $absolute, $slash], + ['param/opt', ['param' => 'value', 'opt' => true], 'valueopt', $absolute, $slash], + ['param/opt', ['param' => 'value', 'opt' => false], 'value', $absolute, $slash], + ['param/opt', ['param' => 'value'], 'value', $absolute, $slash], + ['test7', ['opt' => true], 'testopt', $absolute, $slash], + ['test7', ['opt' => false], 'test', $absolute, $slash], + + ['static1', null, '', $absolute, $slash], + ['static2', null, '', $absolute, $slash], + + ['static3', null, 'test', $absolute, $slash], + ['static4', null, 'test/test2', $absolute, $slash], + ['static5', null, 'test', $absolute, $slash], + ['static6', null, 'test', $absolute, $slash], + ]; + } + + /** + * @dataProvider getBadData + */ + public function testBuildException1($routName, $placeholders) + { + $this->expectException(BadParameterException::class); + $this->urlBuilder->build($routName, $placeholders); + } + + /** + * @dataProvider getBadData + */ + public function testBuildException2($routName, $placeholders) + { + $this->expectException(BadParameterException::class); + $this->urlBuilder->build('/' . $routName, $placeholders); + } + + /** + * @dataProvider getBadData + */ + public function testBuildException3($routName, $placeholders) + { + $this->expectException(BadParameterException::class); + $this->urlBuilder->build($routName, $placeholders, true); + } + + /** + * @dataProvider getBadData + */ + public function testBuildException4($routName, $placeholders) + { + $this->expectException(BadParameterException::class); + $this->urlBuilder->build('/' . $routName, $placeholders, true); + } + + /** + * @return array + */ + public function getBadData() + { + return [ + ['user1', []], + ['user1', ['name' => 'test']], + ['user1', ['id' => 'test']], + + ['user2', ['name' => 'test']], + ['user2', ['id' => 'test', 'name' => 'test']], + ['user2', ['id' => 'test']], + + ['user3', []], + ['user3', ['id' => 'test']], + ['user3', ['id' => 1]], + ['user3', ['id' => 'test', 'name' => 'test']], + ['user3', ['name' => 'test']], + + ['user4', []], + ['user4', ['id' => 'test']], + ['user4', ['id' => 1]], + ['user4', ['id' => 'ids', 'name' => 'test']], + ['user4', ['name' => 'test']], + + ['user5', []], + ['user5', ['id' => 1]], + ['user5', ['id' => 'test']], + ['user5', ['id' => 1, 'name' => 'test']], + ['user5', ['name' => 'test']], + + ['user6', []], + ['user6', ['id' => 'test']], + + ['user7', ['name' => 'test', 'ids' => 1, 'names' => 'tests']], + ['user7', ['id' => 1, 'ids' => 1, 'names' => 'tests']], + ['user7', ['id' => 1, 'name' => 'test', 'names' => 'tests']], + ['user7', ['name' => 'test', 'ids' => 1, 'names' => 'tests']], + ['user7', ['ids' => 1, 'names' => 'tests']], + ['user7', ['names' => 'tests']], + ['user7', ['id' => 'ids', 'name' => 'test', 'ids' => 1, 'names' => 'tests']], + ['user7', ['id' => 'ids', 'ids' => 1, 'names' => 'tests']], + ['user7', ['id' => 'ids', 'name' => 'test', 'names' => 'tests']], + ['user7', ['id' => 'ids', 'ids' => 'bad', 'names' => 'tests']], + + ['user8', ['id' => 1, 'name' => 'test', 'ids' => 'bad', 'names' => 'tests', 'key' => 'bad-key']], + ['user8', ['id' => 1, 'name' => 'test', 'ids' => 'bad', 'names' => 'tests']], + ['user8', ['id' => 1, 'name' => 'test', 'ids' => 'bad', 'key' => 1]], + + ['test1', []], + + ['test4', []], + ['test4', ['param1' => 'test']], + ['test4', ['param2' => 'test']], + + ['test5', ['param' => 50000]], + ['test7', ['opt' => 50000]], + + ['test7', ['opt' => 50000]], + + ['foo-bar', []], + + ['te', []], + + ['param/opt', []], + ['param/opt', ['opt' => true]], + ['param/opt', ['opt' => false]], + + ['static1', ['test' => 40]], + + ['static5', ['opt' => 40]], + + ]; + } + + /** + * @return void + */ + public function testBuildWithQueryArray() + { + $result = $this->urlBuilder->build('/user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => ['key' => 'value'] + ], true); + self::assertEquals(self::BASE_URL . '/user/2/grigor?key=value', $result); + + $result = $this->urlBuilder->build('/user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => ['key' => 'value'] + ]); + self::assertEquals('/user/2/grigor?key=value', $result); + + $result = $this->urlBuilder->build('user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => ['key' => 'value'] + ]); + self::assertEquals('user/2/grigor?key=value', $result); + } + + /** + * @return void + */ + public function testBuildWithQueryString() + { + $result = $this->urlBuilder->build('/user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => 'key=value' + ], true); + self::assertEquals(self::BASE_URL . '/user/2/grigor?key=value', $result); + + $result = $this->urlBuilder->build('/user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => 'key=value' + ]); + self::assertEquals('/user/2/grigor?key=value', $result); + + $result = $this->urlBuilder->build('user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => 'key=value' + ]); + self::assertEquals('user/2/grigor?key=value', $result); + } + + /** + * @return void + */ + public function testBuildWithFragment() + { + $result = $this->urlBuilder->build('/user1', + [ + 'id' => 2, 'name' => 'grigor', + '#' => 'value' + ], true); + self::assertEquals(self::BASE_URL . '/user/2/grigor#value', $result); + + $result = $this->urlBuilder->build('/user1', + [ + 'id' => 2, 'name' => 'grigor', + '#' => 'value' + ]); + self::assertEquals('/user/2/grigor#value', $result); + + $result = $this->urlBuilder->build('user1', + [ + 'id' => 2, 'name' => 'grigor', + '#' => 'value' + ]); + self::assertEquals('user/2/grigor#value', $result); + + $result = $this->urlBuilder->build('user1', + [ + 'id' => 2, 'name' => 'grigor', + ]); + self::assertEquals('user/2/grigor', $result); + } + + /** + * @return void + */ + public function testBuildAllParams() + { + $result = $this->urlBuilder->build('/user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => 'key=value', + '#' => 'value' + ], true); + self::assertEquals(self::BASE_URL . '/user/2/grigor?key=value#value', $result); + + $result = $this->urlBuilder->build('/user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => 'key=value', + '#' => 'value' + ]); + self::assertEquals('/user/2/grigor?key=value#value', $result); + + $result = $this->urlBuilder->build('user1', + [ + 'id' => 2, 'name' => 'grigor', + '?' => 'key=value', + '#' => 'value' + ]); + self::assertEquals('user/2/grigor?key=value#value', $result); + + $result = $this->urlBuilder->build('user1', + [ + 'id' => 2, 'name' => 'grigor', + ]); + self::assertEquals('user/2/grigor', $result); + } + + /** + * @return void + */ + public function testBuildWithIdnConverted() + { + $urlBuilder = new UrlBuilder( + 'https://россия.рф', + new ArrayRulesProvider([ + 'user1' => '/user/{id:\d+}[/{name}]', + ]) + ); + $result = $urlBuilder->build('user1', ['id' => 2, 'name' => 'grigor', ':idn' => true], true); + self::assertEquals('https://xn--h1alffa9f.xn--p1ai/user/2/grigor', $result); + } + + /** + * @return void + */ + public function testBuildWithStrategy() + { + $urlBuilder = new UrlBuilder( + 'https://россия.рф', + new ArrayRulesProvider([ + 'user1' => '/user/{id:\d+}[/{name}]', + ]), + ['referral' => new ReferralUrlStrategy()] + ); + + $result = $urlBuilder->build('/user1', ['id' => 2, 'name' => 'grigor', ':strategy' => 'referral'], true); + self::assertEquals('https://россия.рф/user/2/grigor?refid=222', $result); + + $result = $urlBuilder->build('user1', ['id' => 2, 'name' => 'grigor', ':strategy' => 'referral'], true); + self::assertEquals('https://россия.рф/user/2/grigor?refid=222', $result); + + $result = $urlBuilder->build('/user1', ['id' => 2, 'name' => 'grigor', ':strategy' => 'referral']); + self::assertEquals('/user/2/grigor?refid=222', $result); + + $result = $urlBuilder->build('user1', ['id' => 2, 'name' => 'grigor', ':strategy' => 'referral']); + self::assertEquals('user/2/grigor?refid=222', $result); + } + + /** + * @return void + */ + public function testBuildWithStrategyException() + { + $this->expectException(BadParameterException::class); + $urlBuilder = new UrlBuilder( + 'https://россия.рф', + new ArrayRulesProvider([ + 'user1' => '/user/{id:\d+}[/{name}]', + ]), + ['referral' => new ReferralUrlStrategy()] + ); + + $urlBuilder->build('/user1', ['id' => 2, 'name' => 'grigor', ':strategy' => 'non-existent'], true); + } + + /** + * @return void + */ + public function testConstructEmptyBaseUrl() + { + $this->expectException(InvalidUrlException::class); + new UrlBuilder( + '', + new ArrayRulesProvider([ + 'user1' => '[/{name}]', + ]) + ); + } + + /** + * @return void + */ + public function testConstructEmptyBaseUrl2() + { + $this->expectException(InvalidUrlException::class); + new UrlBuilder( + ' ', + new ArrayRulesProvider([ + 'user1' => '[/{name}]', + ]) + ); + } + +} \ No newline at end of file