Initial commit
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# phpstorm project files
|
||||||
|
.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
|
||||||
|
# local phpunit config
|
||||||
|
/phpunit.xml
|
||||||
|
|
||||||
|
tests/_output/*
|
||||||
|
tests/_support/_generated
|
||||||
|
|
||||||
|
#vagrant folder
|
||||||
|
/.vagrant
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com)
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Yii Software LLC nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||||
|
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/yiisoft" target="_blank">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/993323" height="100px">
|
||||||
|
</a>
|
||||||
|
<h1 align="center">Yii 2 Basic Project Template</h1>
|
||||||
|
<br>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Yii 2 Basic Project Template is a skeleton [Yii 2](https://www.yiiframework.com/) application best for
|
||||||
|
rapidly creating small projects.
|
||||||
|
|
||||||
|
The template contains the basic features including user login/logout and a contact page.
|
||||||
|
It includes all commonly used configurations that would allow you to focus on adding new
|
||||||
|
features to your application.
|
||||||
|
|
||||||
|
[](https://packagist.org/packages/yiisoft/yii2-app-basic)
|
||||||
|
[](https://packagist.org/packages/yiisoft/yii2-app-basic)
|
||||||
|
[](https://github.com/yiisoft/yii2-app-basic/actions?query=workflow%3Abuild)
|
||||||
|
|
||||||
|
DIRECTORY STRUCTURE
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
assets/ contains assets definition
|
||||||
|
commands/ contains console commands (controllers)
|
||||||
|
config/ contains application configurations
|
||||||
|
controllers/ contains Web controller classes
|
||||||
|
mail/ contains view files for e-mails
|
||||||
|
models/ contains model classes
|
||||||
|
runtime/ contains files generated during runtime
|
||||||
|
tests/ contains various tests for the basic application
|
||||||
|
vendor/ contains dependent 3rd-party packages
|
||||||
|
views/ contains view files for the Web application
|
||||||
|
web/ contains the entry script and Web resources
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
REQUIREMENTS
|
||||||
|
------------
|
||||||
|
|
||||||
|
The minimum requirement by this project template that your Web server supports PHP 7.4.
|
||||||
|
|
||||||
|
|
||||||
|
INSTALLATION
|
||||||
|
------------
|
||||||
|
|
||||||
|
### Install via Composer
|
||||||
|
|
||||||
|
If you do not have [Composer](https://getcomposer.org/), you may install it by following the instructions
|
||||||
|
at [getcomposer.org](https://getcomposer.org/doc/00-intro.md#installation-nix).
|
||||||
|
|
||||||
|
You can then install this project template using the following command:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
composer create-project --prefer-dist yiisoft/yii2-app-basic basic
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Now you should be able to access the application through the following URL, assuming `basic` is the directory
|
||||||
|
directly under the Web root.
|
||||||
|
|
||||||
|
~~~
|
||||||
|
http://localhost/basic/web/
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Install from an Archive File
|
||||||
|
|
||||||
|
Extract the archive file downloaded from [yiiframework.com](https://www.yiiframework.com/download/) to
|
||||||
|
a directory named `basic` that is directly under the Web root.
|
||||||
|
|
||||||
|
Set cookie validation key in `config/web.php` file to some random secret string:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'request' => [
|
||||||
|
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
|
||||||
|
'cookieValidationKey' => '<secret random string goes here>',
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then access the application through the following URL:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
http://localhost/basic/web/
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
### Install with Docker
|
||||||
|
|
||||||
|
Update your vendor packages
|
||||||
|
|
||||||
|
docker-compose run --rm php composer update --prefer-dist
|
||||||
|
|
||||||
|
Run the installation triggers (creating cookie validation code)
|
||||||
|
|
||||||
|
docker-compose run --rm php composer install
|
||||||
|
|
||||||
|
Start the container
|
||||||
|
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
You can then access the application through the following URL:
|
||||||
|
|
||||||
|
http://127.0.0.1:8000
|
||||||
|
|
||||||
|
**NOTES:**
|
||||||
|
- Minimum required Docker engine version `17.04` for development (see [Performance tuning for volume mounts](https://docs.docker.com/docker-for-mac/osxfs-caching/))
|
||||||
|
- The default configuration uses a host-volume in your home directory `.docker-composer` for composer caches
|
||||||
|
|
||||||
|
|
||||||
|
CONFIGURATION
|
||||||
|
-------------
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
Edit the file `config/db.php` with real data, for example:
|
||||||
|
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
'class' => 'yii\db\Connection',
|
||||||
|
'dsn' => 'mysql:host=localhost;dbname=yii2basic',
|
||||||
|
'username' => 'root',
|
||||||
|
'password' => '1234',
|
||||||
|
'charset' => 'utf8',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTES:**
|
||||||
|
- Yii won't create the database for you, this has to be done manually before you can access it.
|
||||||
|
- Check and edit the other files in the `config/` directory to customize your application as required.
|
||||||
|
- Refer to the README in the `tests` directory for information specific to basic application tests.
|
||||||
|
|
||||||
|
|
||||||
|
TESTING
|
||||||
|
-------
|
||||||
|
|
||||||
|
Tests are located in `tests` directory. They are developed with [Codeception PHP Testing Framework](https://codeception.com/).
|
||||||
|
By default, there are 3 test suites:
|
||||||
|
|
||||||
|
- `unit`
|
||||||
|
- `functional`
|
||||||
|
- `acceptance`
|
||||||
|
|
||||||
|
Tests can be executed by running
|
||||||
|
|
||||||
|
```
|
||||||
|
vendor/bin/codecept run
|
||||||
|
```
|
||||||
|
|
||||||
|
The command above will execute unit and functional tests. Unit tests are testing the system components, while functional
|
||||||
|
tests are for testing user interaction. Acceptance tests are disabled by default as they require additional setup since
|
||||||
|
they perform testing in real browser.
|
||||||
|
|
||||||
|
|
||||||
|
### Running acceptance tests
|
||||||
|
|
||||||
|
To execute acceptance tests do the following:
|
||||||
|
|
||||||
|
1. Rename `tests/acceptance.suite.yml.example` to `tests/acceptance.suite.yml` to enable suite configuration
|
||||||
|
|
||||||
|
2. Replace `codeception/base` package in `composer.json` with `codeception/codeception` to install full-featured
|
||||||
|
version of Codeception
|
||||||
|
|
||||||
|
3. Update dependencies with Composer
|
||||||
|
|
||||||
|
```
|
||||||
|
composer update
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Download [Selenium Server](https://www.seleniumhq.org/download/) and launch it:
|
||||||
|
|
||||||
|
```
|
||||||
|
java -jar ~/selenium-server-standalone-x.xx.x.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
In case of using Selenium Server 3.0 with Firefox browser since v48 or Google Chrome since v53 you must download [GeckoDriver](https://github.com/mozilla/geckodriver/releases) or [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) and launch Selenium with it:
|
||||||
|
|
||||||
|
```
|
||||||
|
# for Firefox
|
||||||
|
java -jar -Dwebdriver.gecko.driver=~/geckodriver ~/selenium-server-standalone-3.xx.x.jar
|
||||||
|
|
||||||
|
# for Google Chrome
|
||||||
|
java -jar -Dwebdriver.chrome.driver=~/chromedriver ~/selenium-server-standalone-3.xx.x.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
As an alternative way you can use already configured Docker container with older versions of Selenium and Firefox:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --net=host selenium/standalone-firefox:2.53.0
|
||||||
|
```
|
||||||
|
|
||||||
|
5. (Optional) Create `yii2basic_test` database and update it by applying migrations if you have them.
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/bin/yii migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
The database configuration can be found at `config/test_db.php`.
|
||||||
|
|
||||||
|
|
||||||
|
6. Start web server:
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/bin/yii serve
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Now you can run all available tests
|
||||||
|
|
||||||
|
```
|
||||||
|
# run all available tests
|
||||||
|
vendor/bin/codecept run
|
||||||
|
|
||||||
|
# run acceptance tests
|
||||||
|
vendor/bin/codecept run acceptance
|
||||||
|
|
||||||
|
# run only unit and functional tests
|
||||||
|
vendor/bin/codecept run unit,functional
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code coverage support
|
||||||
|
|
||||||
|
By default, code coverage is disabled in `codeception.yml` configuration file, you should uncomment needed rows to be able
|
||||||
|
to collect code coverage. You can run your tests and collect coverage with the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
#collect coverage for all tests
|
||||||
|
vendor/bin/codecept run --coverage --coverage-html --coverage-xml
|
||||||
|
|
||||||
|
#collect coverage only for unit tests
|
||||||
|
vendor/bin/codecept run unit --coverage --coverage-html --coverage-xml
|
||||||
|
|
||||||
|
#collect coverage for unit and functional tests
|
||||||
|
vendor/bin/codecept run functional,unit --coverage --coverage-html --coverage-xml
|
||||||
|
```
|
||||||
|
|
||||||
|
You can see code coverage output under the `tests/_output` directory.
|
||||||
Vendored
+92
@@ -0,0 +1,92 @@
|
|||||||
|
require 'yaml'
|
||||||
|
require 'fileutils'
|
||||||
|
|
||||||
|
required_plugins_installed = nil
|
||||||
|
required_plugins = %w( vagrant-hostmanager vagrant-vbguest )
|
||||||
|
required_plugins.each do |plugin|
|
||||||
|
unless Vagrant.has_plugin? plugin
|
||||||
|
system "vagrant plugin install #{plugin}"
|
||||||
|
required_plugins_installed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# IF plugin[s] was just installed - restart required
|
||||||
|
if required_plugins_installed
|
||||||
|
# Get CLI command[s] and call again
|
||||||
|
system 'vagrant' + ARGV.to_s.gsub(/\[\"|\", \"|\"\]/, ' ')
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
|
domains = {
|
||||||
|
app: 'yii2basic.test'
|
||||||
|
}
|
||||||
|
|
||||||
|
vagrantfile_dir_path = File.dirname(__FILE__)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
local: vagrantfile_dir_path + '/vagrant/config/vagrant-local.yml',
|
||||||
|
example: vagrantfile_dir_path + '/vagrant/config/vagrant-local.example.yml'
|
||||||
|
}
|
||||||
|
|
||||||
|
# copy config from example if local config not exists
|
||||||
|
FileUtils.cp config[:example], config[:local] unless File.exist?(config[:local])
|
||||||
|
# read config
|
||||||
|
options = YAML.load_file config[:local]
|
||||||
|
|
||||||
|
# check github token
|
||||||
|
if options['github_token'].nil? || options['github_token'].to_s.length != 40
|
||||||
|
puts "You must place REAL GitHub token into configuration:\n/yii2-app-basic/vagrant/config/vagrant-local.yml"
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
|
# vagrant configurate
|
||||||
|
Vagrant.configure(2) do |config|
|
||||||
|
# select the box
|
||||||
|
config.vm.box = 'bento/ubuntu-18.04'
|
||||||
|
|
||||||
|
# should we ask about box updates?
|
||||||
|
config.vm.box_check_update = options['box_check_update']
|
||||||
|
|
||||||
|
config.vm.provider 'virtualbox' do |vb|
|
||||||
|
# machine cpus count
|
||||||
|
vb.cpus = options['cpus']
|
||||||
|
# machine memory size
|
||||||
|
vb.memory = options['memory']
|
||||||
|
# machine name (for VirtualBox UI)
|
||||||
|
vb.name = options['machine_name']
|
||||||
|
end
|
||||||
|
|
||||||
|
# machine name (for vagrant console)
|
||||||
|
config.vm.define options['machine_name']
|
||||||
|
|
||||||
|
# machine name (for guest machine console)
|
||||||
|
config.vm.hostname = options['machine_name']
|
||||||
|
|
||||||
|
# network settings
|
||||||
|
config.vm.network 'private_network', ip: options['ip']
|
||||||
|
|
||||||
|
# sync: folder 'yii2-app-advanced' (host machine) -> folder '/app' (guest machine)
|
||||||
|
config.vm.synced_folder './', '/app', owner: 'vagrant', group: 'vagrant'
|
||||||
|
|
||||||
|
# disable folder '/vagrant' (guest machine)
|
||||||
|
config.vm.synced_folder '.', '/vagrant', disabled: true
|
||||||
|
|
||||||
|
# hosts settings (host machine)
|
||||||
|
config.vm.provision :hostmanager
|
||||||
|
config.hostmanager.enabled = true
|
||||||
|
config.hostmanager.manage_host = true
|
||||||
|
config.hostmanager.ignore_private_ip = false
|
||||||
|
config.hostmanager.include_offline = true
|
||||||
|
config.hostmanager.aliases = domains.values
|
||||||
|
|
||||||
|
# quick fix for failed guest additions installations
|
||||||
|
# config.vbguest.auto_update = false
|
||||||
|
|
||||||
|
# provisioners
|
||||||
|
config.vm.provision 'shell', path: './vagrant/provision/once-as-root.sh', args: [options['timezone'], options['ip']]
|
||||||
|
config.vm.provision 'shell', path: './vagrant/provision/once-as-vagrant.sh', args: [options['github_token']], privileged: false
|
||||||
|
config.vm.provision 'shell', path: './vagrant/provision/always-as-root.sh', run: 'always'
|
||||||
|
|
||||||
|
# post-install message (vagrant console)
|
||||||
|
config.vm.post_up_message = "App URL: http://#{domains[:app]}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @link https://www.yiiframework.com/
|
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC
|
||||||
|
* @license https://www.yiiframework.com/license/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace app\assets;
|
||||||
|
|
||||||
|
use yii\web\AssetBundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main application asset bundle.
|
||||||
|
*
|
||||||
|
* @author Qiang Xue <qiang.xue@gmail.com>
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
class AppAsset extends AssetBundle
|
||||||
|
{
|
||||||
|
public $basePath = '@webroot';
|
||||||
|
public $baseUrl = '@web';
|
||||||
|
public $css = [
|
||||||
|
'css/site.css',
|
||||||
|
];
|
||||||
|
public $js = [
|
||||||
|
];
|
||||||
|
public $depends = [
|
||||||
|
'yii\web\YiiAsset',
|
||||||
|
'yii\bootstrap5\BootstrapAsset'
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
actor: Tester
|
||||||
|
bootstrap: _bootstrap.php
|
||||||
|
paths:
|
||||||
|
tests: tests
|
||||||
|
output: tests/_output
|
||||||
|
data: tests/_data
|
||||||
|
helpers: tests/_support
|
||||||
|
settings:
|
||||||
|
memory_limit: 1024M
|
||||||
|
colors: true
|
||||||
|
modules:
|
||||||
|
config:
|
||||||
|
Yii2:
|
||||||
|
configFile: 'config/test.php'
|
||||||
|
|
||||||
|
# To enable code coverage:
|
||||||
|
#coverage:
|
||||||
|
# #c3_url: http://localhost:8080/index-test.php/
|
||||||
|
# enabled: true
|
||||||
|
# #remote: true
|
||||||
|
# #remote_config: '../codeception.yml'
|
||||||
|
# whitelist:
|
||||||
|
# include:
|
||||||
|
# - models/*
|
||||||
|
# - controllers/*
|
||||||
|
# - commands/*
|
||||||
|
# - mail/*
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @link https://www.yiiframework.com/
|
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC
|
||||||
|
* @license https://www.yiiframework.com/license/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace app\commands;
|
||||||
|
|
||||||
|
use yii\console\Controller;
|
||||||
|
use yii\console\ExitCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This command echoes the first argument that you have entered.
|
||||||
|
*
|
||||||
|
* This command is provided as an example for you to learn how to create console commands.
|
||||||
|
*
|
||||||
|
* @author Qiang Xue <qiang.xue@gmail.com>
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
class HelloController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This command echoes what you have entered as the message.
|
||||||
|
* @param string $message the message to be echoed.
|
||||||
|
* @return int Exit code
|
||||||
|
*/
|
||||||
|
public function actionIndex($message = 'hello world')
|
||||||
|
{
|
||||||
|
echo $message . "\n";
|
||||||
|
|
||||||
|
return ExitCode::OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"name": "yiisoft/yii2-app-basic",
|
||||||
|
"description": "Yii 2 Basic Project Template",
|
||||||
|
"keywords": ["yii2", "framework", "basic", "project template"],
|
||||||
|
"homepage": "https://www.yiiframework.com/",
|
||||||
|
"type": "project",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/yiisoft/yii2/issues?state=open",
|
||||||
|
"forum": "https://www.yiiframework.com/forum/",
|
||||||
|
"wiki": "https://www.yiiframework.com/wiki/",
|
||||||
|
"irc": "ircs://irc.libera.chat:6697/yii",
|
||||||
|
"source": "https://github.com/yiisoft/yii2"
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.4.0",
|
||||||
|
"yiisoft/yii2": "~2.0.45",
|
||||||
|
"yiisoft/yii2-bootstrap5": "~2.0.2",
|
||||||
|
"yiisoft/yii2-symfonymailer": "~2.0.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"yiisoft/yii2-debug": "~2.1.0",
|
||||||
|
"yiisoft/yii2-gii": "~2.2.0",
|
||||||
|
"yiisoft/yii2-faker": "~2.0.0",
|
||||||
|
"codeception/codeception": "^5.0.0 || ^4.0",
|
||||||
|
"codeception/lib-innerbrowser": "^4.0 || ^3.0 || ^1.1",
|
||||||
|
"codeception/module-asserts": "^3.0 || ^1.1",
|
||||||
|
"codeception/module-yii2": "^1.1",
|
||||||
|
"codeception/module-filesystem": "^3.0 || ^2.0 || ^1.1",
|
||||||
|
"codeception/verify": "^3.0 || ^2.2"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"yiisoft/yii2-composer" : true
|
||||||
|
},
|
||||||
|
"process-timeout": 1800,
|
||||||
|
"fxp-asset": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-install-cmd": [
|
||||||
|
"yii\\composer\\Installer::postInstall"
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"yii\\composer\\Installer::postCreateProject",
|
||||||
|
"yii\\composer\\Installer::postInstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"yii\\composer\\Installer::postCreateProject": {
|
||||||
|
"setPermission": [
|
||||||
|
{
|
||||||
|
"runtime": "0777",
|
||||||
|
"web/assets": "0777",
|
||||||
|
"yii": "0755"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"yii\\composer\\Installer::postInstall": {
|
||||||
|
"generateCookieValidationKey": [
|
||||||
|
"config/web.php"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "composer",
|
||||||
|
"url": "https://asset-packagist.org"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+5493
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class only exists here for IDE (PHPStorm/Netbeans/...) autocompletion.
|
||||||
|
* This file is never included anywhere.
|
||||||
|
* Adjust this file to match classes configured in your application config, to enable IDE autocompletion for custom components.
|
||||||
|
* Example: A property phpdoc can be added in `__Application` class as `@property \vendor\package\Rollbar|__Rollbar $rollbar` and adding a class in this file
|
||||||
|
* ```php
|
||||||
|
* // @property of \vendor\package\Rollbar goes here
|
||||||
|
* class __Rollbar {
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class Yii {
|
||||||
|
/**
|
||||||
|
* @var \yii\web\Application|\yii\console\Application|__Application
|
||||||
|
*/
|
||||||
|
public static $app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property yii\rbac\DbManager $authManager
|
||||||
|
* @property \yii\web\User|__WebUser $user
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class __Application {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property app\models\User $identity
|
||||||
|
*/
|
||||||
|
class __WebUser {
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$params = require __DIR__ . '/params.php';
|
||||||
|
$db = require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
$config = [
|
||||||
|
'id' => 'basic-console',
|
||||||
|
'basePath' => dirname(__DIR__),
|
||||||
|
'bootstrap' => ['log'],
|
||||||
|
'controllerNamespace' => 'app\commands',
|
||||||
|
'aliases' => [
|
||||||
|
'@bower' => '@vendor/bower-asset',
|
||||||
|
'@npm' => '@vendor/npm-asset',
|
||||||
|
'@tests' => '@app/tests',
|
||||||
|
],
|
||||||
|
'components' => [
|
||||||
|
'cache' => [
|
||||||
|
'class' => 'yii\caching\FileCache',
|
||||||
|
],
|
||||||
|
'log' => [
|
||||||
|
'targets' => [
|
||||||
|
[
|
||||||
|
'class' => 'yii\log\FileTarget',
|
||||||
|
'levels' => ['error', 'warning'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'db' => $db,
|
||||||
|
],
|
||||||
|
'params' => $params,
|
||||||
|
/*
|
||||||
|
'controllerMap' => [
|
||||||
|
'fixture' => [ // Fixture generation command line.
|
||||||
|
'class' => 'yii\faker\FixtureController',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
];
|
||||||
|
|
||||||
|
if (YII_ENV_DEV) {
|
||||||
|
// configuration adjustments for 'dev' environment
|
||||||
|
$config['bootstrap'][] = 'gii';
|
||||||
|
$config['modules']['gii'] = [
|
||||||
|
'class' => 'yii\gii\Module',
|
||||||
|
];
|
||||||
|
// configuration adjustments for 'dev' environment
|
||||||
|
// requires version `2.1.21` of yii2-debug module
|
||||||
|
$config['bootstrap'][] = 'debug';
|
||||||
|
$config['modules']['debug'] = [
|
||||||
|
'class' => 'yii\debug\Module',
|
||||||
|
// uncomment the following to add your IP if you are not connecting from localhost.
|
||||||
|
//'allowedIPs' => ['127.0.0.1', '::1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'class' => 'yii\db\Connection',
|
||||||
|
'dsn' => 'mysql:host=localhost;dbname=yii2basic',
|
||||||
|
'username' => 'root',
|
||||||
|
'password' => '',
|
||||||
|
'charset' => 'utf8',
|
||||||
|
|
||||||
|
// Schema cache options (for production environment)
|
||||||
|
//'enableSchemaCache' => true,
|
||||||
|
//'schemaCacheDuration' => 60,
|
||||||
|
//'schemaCache' => 'cache',
|
||||||
|
];
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'adminEmail' => 'admin@example.com',
|
||||||
|
'senderEmail' => 'noreply@example.com',
|
||||||
|
'senderName' => 'Example.com mailer',
|
||||||
|
];
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
$params = require __DIR__ . '/params.php';
|
||||||
|
$db = require __DIR__ . '/test_db.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application configuration shared by all test types
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'id' => 'basic-tests',
|
||||||
|
'basePath' => dirname(__DIR__),
|
||||||
|
'aliases' => [
|
||||||
|
'@bower' => '@vendor/bower-asset',
|
||||||
|
'@npm' => '@vendor/npm-asset',
|
||||||
|
],
|
||||||
|
'language' => 'en-US',
|
||||||
|
'components' => [
|
||||||
|
'db' => $db,
|
||||||
|
'mailer' => [
|
||||||
|
'class' => \yii\symfonymailer\Mailer::class,
|
||||||
|
'viewPath' => '@app/mail',
|
||||||
|
// send all mails to a file by default.
|
||||||
|
'useFileTransport' => true,
|
||||||
|
'messageClass' => 'yii\symfonymailer\Message'
|
||||||
|
],
|
||||||
|
'assetManager' => [
|
||||||
|
'basePath' => __DIR__ . '/../web/assets',
|
||||||
|
],
|
||||||
|
'urlManager' => [
|
||||||
|
'showScriptName' => true,
|
||||||
|
],
|
||||||
|
'user' => [
|
||||||
|
'identityClass' => 'app\models\User',
|
||||||
|
],
|
||||||
|
'request' => [
|
||||||
|
'cookieValidationKey' => 'test',
|
||||||
|
'enableCsrfValidation' => false,
|
||||||
|
// but if you absolutely need it set cookie domain to localhost
|
||||||
|
/*
|
||||||
|
'csrfCookie' => [
|
||||||
|
'domain' => 'localhost',
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'params' => $params,
|
||||||
|
];
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
$db = require __DIR__ . '/db.php';
|
||||||
|
// test database! Important not to run tests on production or development databases
|
||||||
|
$db['dsn'] = 'mysql:host=localhost;dbname=yii2basic_test';
|
||||||
|
|
||||||
|
return $db;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$params = require __DIR__ . '/params.php';
|
||||||
|
$db = require __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
$config = [
|
||||||
|
'id' => 'basic',
|
||||||
|
'basePath' => dirname(__DIR__),
|
||||||
|
'bootstrap' => ['log'],
|
||||||
|
'aliases' => [
|
||||||
|
'@bower' => '@vendor/bower-asset',
|
||||||
|
'@npm' => '@vendor/npm-asset',
|
||||||
|
],
|
||||||
|
'components' => [
|
||||||
|
'request' => [
|
||||||
|
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
|
||||||
|
'cookieValidationKey' => 'wyfZNsArEJYqB8EzG5Ag7kphYr_KDZqb',
|
||||||
|
],
|
||||||
|
'cache' => [
|
||||||
|
'class' => 'yii\caching\FileCache',
|
||||||
|
],
|
||||||
|
'user' => [
|
||||||
|
'identityClass' => 'app\models\User',
|
||||||
|
'enableAutoLogin' => true,
|
||||||
|
],
|
||||||
|
'errorHandler' => [
|
||||||
|
'errorAction' => 'site/error',
|
||||||
|
],
|
||||||
|
'mailer' => [
|
||||||
|
'class' => \yii\symfonymailer\Mailer::class,
|
||||||
|
'viewPath' => '@app/mail',
|
||||||
|
// send all mails to a file by default.
|
||||||
|
'useFileTransport' => true,
|
||||||
|
],
|
||||||
|
'log' => [
|
||||||
|
'traceLevel' => YII_DEBUG ? 3 : 0,
|
||||||
|
'targets' => [
|
||||||
|
[
|
||||||
|
'class' => 'yii\log\FileTarget',
|
||||||
|
'levels' => ['error', 'warning'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'db' => $db,
|
||||||
|
/*
|
||||||
|
'urlManager' => [
|
||||||
|
'enablePrettyUrl' => true,
|
||||||
|
'showScriptName' => false,
|
||||||
|
'rules' => [
|
||||||
|
],
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
],
|
||||||
|
'params' => $params,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (YII_ENV_DEV) {
|
||||||
|
// configuration adjustments for 'dev' environment
|
||||||
|
$config['bootstrap'][] = 'debug';
|
||||||
|
$config['modules']['debug'] = [
|
||||||
|
'class' => 'yii\debug\Module',
|
||||||
|
// uncomment the following to add your IP if you are not connecting from localhost.
|
||||||
|
//'allowedIPs' => ['127.0.0.1', '::1'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$config['bootstrap'][] = 'gii';
|
||||||
|
$config['modules']['gii'] = [
|
||||||
|
'class' => 'yii\gii\Module',
|
||||||
|
// uncomment the following to add your IP if you are not connecting from localhost.
|
||||||
|
//'allowedIPs' => ['127.0.0.1', '::1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controllers;
|
||||||
|
|
||||||
|
use Yii;
|
||||||
|
use yii\filters\AccessControl;
|
||||||
|
use yii\web\Controller;
|
||||||
|
use yii\web\Response;
|
||||||
|
use yii\filters\VerbFilter;
|
||||||
|
use app\models\LoginForm;
|
||||||
|
use app\models\ContactForm;
|
||||||
|
|
||||||
|
class SiteController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function behaviors()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'access' => [
|
||||||
|
'class' => AccessControl::class,
|
||||||
|
'only' => ['logout'],
|
||||||
|
'rules' => [
|
||||||
|
[
|
||||||
|
'actions' => ['logout'],
|
||||||
|
'allow' => true,
|
||||||
|
'roles' => ['@'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'verbs' => [
|
||||||
|
'class' => VerbFilter::class,
|
||||||
|
'actions' => [
|
||||||
|
'logout' => ['post'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function actions()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'error' => [
|
||||||
|
'class' => 'yii\web\ErrorAction',
|
||||||
|
],
|
||||||
|
'captcha' => [
|
||||||
|
'class' => 'yii\captcha\CaptchaAction',
|
||||||
|
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays homepage.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function actionIndex()
|
||||||
|
{
|
||||||
|
return $this->render('index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login action.
|
||||||
|
*
|
||||||
|
* @return Response|string
|
||||||
|
*/
|
||||||
|
public function actionLogin()
|
||||||
|
{
|
||||||
|
if (!Yii::$app->user->isGuest) {
|
||||||
|
return $this->goHome();
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = new LoginForm();
|
||||||
|
if ($model->load(Yii::$app->request->post()) && $model->login()) {
|
||||||
|
return $this->goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
$model->password = '';
|
||||||
|
return $this->render('login', [
|
||||||
|
'model' => $model,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout action.
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function actionLogout()
|
||||||
|
{
|
||||||
|
Yii::$app->user->logout();
|
||||||
|
|
||||||
|
return $this->goHome();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays contact page.
|
||||||
|
*
|
||||||
|
* @return Response|string
|
||||||
|
*/
|
||||||
|
public function actionContact()
|
||||||
|
{
|
||||||
|
$model = new ContactForm();
|
||||||
|
if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
|
||||||
|
Yii::$app->session->setFlash('contactFormSubmitted');
|
||||||
|
|
||||||
|
return $this->refresh();
|
||||||
|
}
|
||||||
|
return $this->render('contact', [
|
||||||
|
'model' => $model,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays about page.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function actionAbout()
|
||||||
|
{
|
||||||
|
return $this->render('about');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
php:
|
||||||
|
image: yiisoftware/yii2-php:7.4-apache
|
||||||
|
volumes:
|
||||||
|
- ~/.composer-docker/cache:/root/.composer/cache:delegated
|
||||||
|
- ./:/app:delegated
|
||||||
|
ports:
|
||||||
|
- '8000:80'
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
use yii\helpers\Html;
|
||||||
|
|
||||||
|
/** @var \yii\web\View $this view component instance */
|
||||||
|
/** @var \yii\mail\MessageInterface $message the message being composed */
|
||||||
|
/** @var string $content main view render result */
|
||||||
|
?>
|
||||||
|
<?php $this->beginPage() ?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=<?= Yii::$app->charset ?>" />
|
||||||
|
<title><?= Html::encode($this->title) ?></title>
|
||||||
|
<?php $this->head() ?>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php $this->beginBody() ?>
|
||||||
|
<?= $content ?>
|
||||||
|
<?php $this->endBody() ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php $this->endPage() ?>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var yii\web\View $this view component instance
|
||||||
|
* @var yii\mail\BaseMessage $message the message being composed
|
||||||
|
* @var string $content main view render result
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->beginPage();
|
||||||
|
$this->beginBody();
|
||||||
|
echo $content;
|
||||||
|
$this->endBody();
|
||||||
|
$this->endPage();
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\models;
|
||||||
|
|
||||||
|
use Yii;
|
||||||
|
use yii\base\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ContactForm is the model behind the contact form.
|
||||||
|
*/
|
||||||
|
class ContactForm extends Model
|
||||||
|
{
|
||||||
|
public $name;
|
||||||
|
public $email;
|
||||||
|
public $subject;
|
||||||
|
public $body;
|
||||||
|
public $verifyCode;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array the validation rules.
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// name, email, subject and body are required
|
||||||
|
[['name', 'email', 'subject', 'body'], 'required'],
|
||||||
|
// email has to be a valid email address
|
||||||
|
['email', 'email'],
|
||||||
|
// verifyCode needs to be entered correctly
|
||||||
|
['verifyCode', 'captcha'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array customized attribute labels
|
||||||
|
*/
|
||||||
|
public function attributeLabels()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'verifyCode' => 'Verification Code',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an email to the specified email address using the information collected by this model.
|
||||||
|
* @param string $email the target email address
|
||||||
|
* @return bool whether the model passes validation
|
||||||
|
*/
|
||||||
|
public function contact($email)
|
||||||
|
{
|
||||||
|
if ($this->validate()) {
|
||||||
|
Yii::$app->mailer->compose()
|
||||||
|
->setTo($email)
|
||||||
|
->setFrom([Yii::$app->params['senderEmail'] => Yii::$app->params['senderName']])
|
||||||
|
->setReplyTo([$this->email => $this->name])
|
||||||
|
->setSubject($this->subject)
|
||||||
|
->setTextBody($this->body)
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\models;
|
||||||
|
|
||||||
|
use Yii;
|
||||||
|
use yii\base\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LoginForm is the model behind the login form.
|
||||||
|
*
|
||||||
|
* @property-read User|null $user
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class LoginForm extends Model
|
||||||
|
{
|
||||||
|
public $username;
|
||||||
|
public $password;
|
||||||
|
public $rememberMe = true;
|
||||||
|
|
||||||
|
private $_user = false;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array the validation rules.
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// username and password are both required
|
||||||
|
[['username', 'password'], 'required'],
|
||||||
|
// rememberMe must be a boolean value
|
||||||
|
['rememberMe', 'boolean'],
|
||||||
|
// password is validated by validatePassword()
|
||||||
|
['password', 'validatePassword'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the password.
|
||||||
|
* This method serves as the inline validation for password.
|
||||||
|
*
|
||||||
|
* @param string $attribute the attribute currently being validated
|
||||||
|
* @param array $params the additional name-value pairs given in the rule
|
||||||
|
*/
|
||||||
|
public function validatePassword($attribute, $params)
|
||||||
|
{
|
||||||
|
if (!$this->hasErrors()) {
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if (!$user || !$user->validatePassword($this->password)) {
|
||||||
|
$this->addError($attribute, 'Incorrect username or password.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs in a user using the provided username and password.
|
||||||
|
* @return bool whether the user is logged in successfully
|
||||||
|
*/
|
||||||
|
public function login()
|
||||||
|
{
|
||||||
|
if ($this->validate()) {
|
||||||
|
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds user by [[username]]
|
||||||
|
*
|
||||||
|
* @return User|null
|
||||||
|
*/
|
||||||
|
public function getUser()
|
||||||
|
{
|
||||||
|
if ($this->_user === false) {
|
||||||
|
$this->_user = User::findByUsername($this->username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_user;
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\models;
|
||||||
|
|
||||||
|
class User extends \yii\base\BaseObject implements \yii\web\IdentityInterface
|
||||||
|
{
|
||||||
|
public $id;
|
||||||
|
public $username;
|
||||||
|
public $password;
|
||||||
|
public $authKey;
|
||||||
|
public $accessToken;
|
||||||
|
|
||||||
|
private static $users = [
|
||||||
|
'100' => [
|
||||||
|
'id' => '100',
|
||||||
|
'username' => 'admin',
|
||||||
|
'password' => 'admin',
|
||||||
|
'authKey' => 'test100key',
|
||||||
|
'accessToken' => '100-token',
|
||||||
|
],
|
||||||
|
'101' => [
|
||||||
|
'id' => '101',
|
||||||
|
'username' => 'demo',
|
||||||
|
'password' => 'demo',
|
||||||
|
'authKey' => 'test101key',
|
||||||
|
'accessToken' => '101-token',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function findIdentity($id)
|
||||||
|
{
|
||||||
|
return isset(self::$users[$id]) ? new static(self::$users[$id]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function findIdentityByAccessToken($token, $type = null)
|
||||||
|
{
|
||||||
|
foreach (self::$users as $user) {
|
||||||
|
if ($user['accessToken'] === $token) {
|
||||||
|
return new static($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds user by username
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
* @return static|null
|
||||||
|
*/
|
||||||
|
public static function findByUsername($username)
|
||||||
|
{
|
||||||
|
foreach (self::$users as $user) {
|
||||||
|
if (strcasecmp($user['username'], $username) === 0) {
|
||||||
|
return new static($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getId()
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getAuthKey()
|
||||||
|
{
|
||||||
|
return $this->authKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function validateAuthKey($authKey)
|
||||||
|
{
|
||||||
|
return $this->authKey === $authKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates password
|
||||||
|
*
|
||||||
|
* @param string $password password to validate
|
||||||
|
* @return bool if password provided is valid for current user
|
||||||
|
*/
|
||||||
|
public function validatePassword($password)
|
||||||
|
{
|
||||||
|
return $this->password === $password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Application requirement checker script.
|
||||||
|
*
|
||||||
|
* In order to run this script use the following console command:
|
||||||
|
* php requirements.php
|
||||||
|
*
|
||||||
|
* In order to run this script from the web, you should copy it to the web root.
|
||||||
|
* If you are using Linux you can create a hard link instead, using the following command:
|
||||||
|
* ln ../requirements.php requirements.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
// you may need to adjust this path to the correct Yii framework path
|
||||||
|
// uncomment and adjust the following line if Yii is not located at the default path
|
||||||
|
//$frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2';
|
||||||
|
|
||||||
|
|
||||||
|
if (!isset($frameworkPath)) {
|
||||||
|
$searchPaths = array(
|
||||||
|
dirname(__FILE__) . '/vendor/yiisoft/yii2',
|
||||||
|
dirname(__FILE__) . '/../vendor/yiisoft/yii2',
|
||||||
|
);
|
||||||
|
foreach ($searchPaths as $path) {
|
||||||
|
if (is_dir($path)) {
|
||||||
|
$frameworkPath = $path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($frameworkPath) || !is_dir($frameworkPath)) {
|
||||||
|
$message = "<h1>Error</h1>\n\n"
|
||||||
|
. "<p><strong>The path to yii framework seems to be incorrect.</strong></p>\n"
|
||||||
|
. '<p>You need to install Yii framework via composer or adjust the framework path in file <abbr title="' . __FILE__ . '">' . basename(__FILE__) . "</abbr>.</p>\n"
|
||||||
|
. '<p>Please refer to the <abbr title="' . dirname(__FILE__) . "/README.md\">README</abbr> on how to install Yii.</p>\n";
|
||||||
|
|
||||||
|
if (!empty($_SERVER['argv'])) {
|
||||||
|
// do not print HTML when used in console mode
|
||||||
|
echo strip_tags($message);
|
||||||
|
} else {
|
||||||
|
echo $message;
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once($frameworkPath . '/requirements/YiiRequirementChecker.php');
|
||||||
|
$requirementsChecker = new YiiRequirementChecker();
|
||||||
|
|
||||||
|
$gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.';
|
||||||
|
$gdOK = $imagickOK = false;
|
||||||
|
|
||||||
|
if (extension_loaded('imagick')) {
|
||||||
|
$imagick = new Imagick();
|
||||||
|
$imagickFormats = $imagick->queryFormats('PNG');
|
||||||
|
if (in_array('PNG', $imagickFormats)) {
|
||||||
|
$imagickOK = true;
|
||||||
|
} else {
|
||||||
|
$imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension_loaded('gd')) {
|
||||||
|
$gdInfo = gd_info();
|
||||||
|
if (!empty($gdInfo['FreeType Support'])) {
|
||||||
|
$gdOK = true;
|
||||||
|
} else {
|
||||||
|
$gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust requirements according to your application specifics.
|
||||||
|
*/
|
||||||
|
$requirements = array(
|
||||||
|
// Database :
|
||||||
|
array(
|
||||||
|
'name' => 'PDO extension',
|
||||||
|
'mandatory' => true,
|
||||||
|
'condition' => extension_loaded('pdo'),
|
||||||
|
'by' => 'All DB-related classes',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'PDO SQLite extension',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => extension_loaded('pdo_sqlite'),
|
||||||
|
'by' => 'All DB-related classes',
|
||||||
|
'memo' => 'Required for SQLite database.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'PDO MySQL extension',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => extension_loaded('pdo_mysql'),
|
||||||
|
'by' => 'All DB-related classes',
|
||||||
|
'memo' => 'Required for MySQL database.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'PDO PostgreSQL extension',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => extension_loaded('pdo_pgsql'),
|
||||||
|
'by' => 'All DB-related classes',
|
||||||
|
'memo' => 'Required for PostgreSQL database.',
|
||||||
|
),
|
||||||
|
// Cache :
|
||||||
|
array(
|
||||||
|
'name' => 'Memcache extension',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => extension_loaded('memcache') || extension_loaded('memcached'),
|
||||||
|
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-caching-memcache.html">MemCache</a>',
|
||||||
|
'memo' => extension_loaded('memcached') ? 'To use memcached set <a href="https://www.yiiframework.com/doc-2.0/yii-caching-memcache.html#$useMemcached-detail">MemCache::useMemcached</a> to <code>true</code>.' : ''
|
||||||
|
),
|
||||||
|
// CAPTCHA:
|
||||||
|
array(
|
||||||
|
'name' => 'GD PHP extension with FreeType support',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => $gdOK,
|
||||||
|
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
|
||||||
|
'memo' => $gdMemo,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'ImageMagick PHP extension with PNG support',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => $imagickOK,
|
||||||
|
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
|
||||||
|
'memo' => $imagickMemo,
|
||||||
|
),
|
||||||
|
// PHP ini :
|
||||||
|
'phpExposePhp' => array(
|
||||||
|
'name' => 'Expose PHP',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => $requirementsChecker->checkPhpIniOff("expose_php"),
|
||||||
|
'by' => 'Security reasons',
|
||||||
|
'memo' => '"expose_php" should be disabled at php.ini',
|
||||||
|
),
|
||||||
|
'phpAllowUrlInclude' => array(
|
||||||
|
'name' => 'PHP allow url include',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"),
|
||||||
|
'by' => 'Security reasons',
|
||||||
|
'memo' => '"allow_url_include" should be disabled at php.ini',
|
||||||
|
),
|
||||||
|
'phpSmtp' => array(
|
||||||
|
'name' => 'PHP mail SMTP',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => strlen(ini_get('SMTP')) > 0,
|
||||||
|
'by' => 'Email sending',
|
||||||
|
'memo' => 'PHP mail SMTP server required',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// OPcache check
|
||||||
|
if (!version_compare(phpversion(), '5.5', '>=')) {
|
||||||
|
$requirements[] = array(
|
||||||
|
'name' => 'APC extension',
|
||||||
|
'mandatory' => false,
|
||||||
|
'condition' => extension_loaded('apc'),
|
||||||
|
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-caching-apccache.html">ApcCache</a>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $requirementsChecker->checkYii()->check($requirements)->getResult();
|
||||||
|
$requirementsChecker->render();
|
||||||
|
exit($result['summary']['errors'] === 0 ? 0 : 1);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload.php @generated by Composer
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 50600) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, $err);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo $err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new RuntimeException($err);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/composer/autoload_real.php';
|
||||||
|
|
||||||
|
return ComposerAutoloaderInit7309b9d2e4518ad52988e780a875afc0::getLoader();
|
||||||
Vendored
+518
@@ -0,0 +1,518 @@
|
|||||||
|
# Change Log
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
This project follows the [Behat release and version support policies]
|
||||||
|
(https://docs.behat.org/en/latest/releases.html).
|
||||||
|
|
||||||
|
# [4.14.0] - 2025-05-23
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Throw ParserException if file ends with tags by @acoulton in [#313](https://github.com/Behat/Gherkin/pull/313)
|
||||||
|
* Throw ParserException if Background comes after first Scenario by @acoulton in [#343](https://github.com/Behat/Gherkin/pull/343)
|
||||||
|
* For compatibility with the official cucumber/gherkin parsers, we now accept some gherkin syntax that would previously
|
||||||
|
have triggered a ParserException. Users may wish to consider running a tool like gherkin-lint in CI to detect
|
||||||
|
incomplete feature files or valid-but-unusual gherkin syntax. The specific changes are:
|
||||||
|
- Parse `Scenario` and `Scenario Outline` as synonyms depending on the presence (or not) of an `Examples:` keyword.
|
||||||
|
by @acoulton in [#316](https://github.com/Behat/Gherkin/pull/316) and [#324](https://github.com/Behat/Gherkin/pull/324)
|
||||||
|
- Do not throw on some unexpected Feature / Language tags by @acoulton in [#323](https://github.com/Behat/Gherkin/pull/323)
|
||||||
|
- Do not throw on `.feature` file that does not contain a Feature by @acoulton in [#340](https://github.com/Behat/Gherkin/pull/340)
|
||||||
|
- Ignore content after table right-hand `|` (instead of throwing) by @acoulton in [#341](https://github.com/Behat/Gherkin/pull/341)
|
||||||
|
* Remove the line length from the NewLine token value by @stof in [#338](https://github.com/Behat/Gherkin/pull/338)
|
||||||
|
* Added precise PHPStan type information by @stof in [#332](https://github.com/Behat/Gherkin/pull/332),
|
||||||
|
[#333](https://github.com/Behat/Gherkin/pull/333), [#339](https://github.com/Behat/Gherkin/pull/339)
|
||||||
|
and [#334](https://github.com/Behat/Gherkin/pull/334)
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
* Make private props readonly; fix tests by @uuf6429 in [#319](https://github.com/Behat/Gherkin/pull/319)
|
||||||
|
* Use the `Yaml::parseFile` API to handle Yaml files by @stof in [#335](https://github.com/Behat/Gherkin/pull/335)
|
||||||
|
* test: Make CucumberND name reading consistent by @uuf6429 in [#309](https://github.com/Behat/Gherkin/pull/309)
|
||||||
|
* test: Use vfsStream to simplify / improve filesystem-related tests by @uuf6429 in [#298](https://github.com/Behat/Gherkin/pull/298)
|
||||||
|
* test: Handle optional tableHeader when loading NDJson examples by @uuf6429 in [#294](https://github.com/Behat/Gherkin/pull/294)
|
||||||
|
* test: Refactor valid ParserExceptionsTest examples into cucumber/gherkin testdata by @acoulton in [#322](https://github.com/Behat/Gherkin/pull/322)
|
||||||
|
* test: Compare step arguments when checking gherkin parity by @acoulton in [#325](https://github.com/Behat/Gherkin/pull/325)
|
||||||
|
* test: Use a custom object comparator to ignore the keywordType of StepNode by @stof in [#331](https://github.com/Behat/Gherkin/pull/331)
|
||||||
|
* ci: Add conventional title to gherkin update, error on missing asserts by @acoulton in [#314](https://github.com/Behat/Gherkin/pull/314)
|
||||||
|
* Assert that preg_split does not fail when splitting a table row by @stof in [#337](https://github.com/Behat/Gherkin/pull/337)
|
||||||
|
* Add assertions in the parser to reflect the structure of tokens by @stof in [#342](https://github.com/Behat/Gherkin/pull/342)
|
||||||
|
* style: Define and change phpdoc order coding style by @uuf6429 in [#345](https://github.com/Behat/Gherkin/pull/345)
|
||||||
|
|
||||||
|
|
||||||
|
# [4.13.0] - 2025-05-06
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Files have been moved to flatten paths into a PSR-4 structure (instead of the previous PSR-0). This may affect users
|
||||||
|
who are requiring files directly rather than using the composer autoloader as expected.
|
||||||
|
See the 4.12.0 release for the new `CachedArrayKeywords::withDefaultKeywords()` to use the `i18n.php` file without
|
||||||
|
depending on paths to other files in this repo. By @uuf6429 in [#288](https://github.com/Behat/Gherkin/pull/288)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* ExampleTableNode now implements TaggedNodeInterface. Also refactored node tag handling methods. By @uuf6429 in
|
||||||
|
[#289](https://github.com/Behat/Gherkin/pull/289)
|
||||||
|
* Improve some exceptions thrown when parsing invalid feature files. Also increased test coverage. By @uuf6429 in
|
||||||
|
[#295](https://github.com/Behat/Gherkin/pull/295)
|
||||||
|
* New translations for `amh` (Amharic), `be` (Belarusian) and `ml` (Malayalam) from cucumber/gherkin in [#306](https://github.com/Behat/Gherkin/pull/306)
|
||||||
|
* Improved translations / whitespace for `ga` (Irish), `it` (Italian), `ja` (Japanese), `ka` (Georgian) and `ko` (Korean)
|
||||||
|
from cucumber/gherkin in [#306](https://github.com/Behat/Gherkin/pull/306)
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
* Fix & improve automatic CI updates to newer cucumber/gherkin test data and translations. By @acoulton in
|
||||||
|
[#300](https://github.com/Behat/Gherkin/pull/300), [#302](https://github.com/Behat/Gherkin/pull/302),
|
||||||
|
[#304](https://github.com/Behat/Gherkin/pull/304), [#305](https://github.com/Behat/Gherkin/pull/305)
|
||||||
|
* Update code style and resolve PHPStan warnings (up to level 9) in tests and CI scripts. By @uuf6429 in
|
||||||
|
[#296](https://github.com/Behat/Gherkin/pull/296), [#297](https://github.com/Behat/Gherkin/pull/297)
|
||||||
|
and [#307](https://github.com/Behat/Gherkin/pull/307)
|
||||||
|
* Make tests that expect exceptions more explicit by @uuf6429 in [#310](https://github.com/Behat/Gherkin/pull/310)
|
||||||
|
* Improve CI workflows and integrate Codecov reporting by @uuf6429 in [#299](https://github.com/Behat/Gherkin/pull/299)
|
||||||
|
and [#301](https://github.com/Behat/Gherkin/pull/301)
|
||||||
|
* Refactor tag filtering implementation by @uuf6429 in [#308](https://github.com/Behat/Gherkin/pull/308)
|
||||||
|
* Update cucumber/gherkin parity tests to v32.1.1 in [#306](https://github.com/Behat/Gherkin/pull/306)
|
||||||
|
|
||||||
|
# [4.12.0] - 2025-02-26
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Gherkin::VERSION is deprecated and will not be updated, use the composer runtime API if you need to identify the
|
||||||
|
running version. This also changes the value used to namespace cached feature files.
|
||||||
|
by @acoulton in [#279](https://github.com/Behat/Gherkin/pull/279)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Provide `CachedArrayKeywords::withDefaultKeywords()` to create an instance without an external dependency on the path
|
||||||
|
to the `i18n.php` file in this repo. **NOTE** that paths to source files will change in the next Gherkin release -
|
||||||
|
use the new constructor to avoid any impact.
|
||||||
|
by @carlos-granados in [#290](https://github.com/Behat/Gherkin/pull/290)
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
* Upgrade to phpunit 10 by @uuf6429 in [#275](https://github.com/Behat/Gherkin/pull/275)
|
||||||
|
* Remove redundant files by @uuf6429 in [#278](https://github.com/Behat/Gherkin/pull/278)
|
||||||
|
* Update documentation by @uuf6429 in [#274](https://github.com/Behat/Gherkin/pull/274)
|
||||||
|
* Adopt PHP CS Fixer and apply code styles by @uuf6429 in [#277](https://github.com/Behat/Gherkin/pull/277)
|
||||||
|
* Add PHPStan and improve / fix docblock annotations and type-safety within methods to achieve level 5 by
|
||||||
|
@uuf6429 in [#276](https://github.com/Behat/Gherkin/pull/276), [#281](https://github.com/Behat/Gherkin/pull/281),
|
||||||
|
[#282](https://github.com/Behat/Gherkin/pull/282), and [#287](https://github.com/Behat/Gherkin/pull/287)
|
||||||
|
|
||||||
|
# [4.11.0] - 2024-12-06
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Drop support for PHP < 8.1, Symfony < 5.4 and Symfony 6.0 - 6.3. In future we will drop support for PHP and symfony
|
||||||
|
versions as they reach EOL. by @acoulton in [#272](https://github.com/Behat/Gherkin/pull/272)
|
||||||
|
* Deprecated `ExampleNode::getTitle()` and `ScenarioNode::getTitle()` in favour of new methods with clearer meaning.
|
||||||
|
by @uuf6429 in [#271](https://github.com/Behat/Gherkin/pull/271)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Added `(ExampleNode|ScenarioNode)::getName()` to access human-readable names for examples and scenarios,
|
||||||
|
and `ExampleNode::getExampleText()` for the string content of the example table row.
|
||||||
|
by @uuf6429 in [#271](https://github.com/Behat/Gherkin/pull/271)
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
|
||||||
|
* Enable dependabot for github actions workflows by @jrfnl in [#261](https://github.com/Behat/Gherkin/pull/261)
|
||||||
|
|
||||||
|
# 4.10.0 / 2024-10-19
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **⚠ Backslashes in feature files must now be escaped**\
|
||||||
|
Gherkin syntax treats `\` as an escape character, which must be escaped (`\\`) to use it as a
|
||||||
|
literal value. Historically, this was not being parsed correctly. This release fixes that bug,
|
||||||
|
but means that if your scenarios currently use unescaped `\` you will need to replace each one
|
||||||
|
with `\\` to achieve the same parsed result.
|
||||||
|
By @everzet in 5a0836d.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Symfony 6 and 7 thanks to @tacman in #257
|
||||||
|
- PHP 8.4 support thanks to @heiglandreas in #258 and @jrfnl in #262
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix exception when filter string is empty thanks to @magikid in #251
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Sync teststuite with Cucumber 24.1.0
|
||||||
|
- Fix PHPUnit 10 deprecation messages
|
||||||
|
- A lot of great CI work by @heiglandreas and @jrfnl
|
||||||
|
|
||||||
|
# 4.9.0 / 2021-10-12
|
||||||
|
|
||||||
|
- Simplify the boolean condition for the tag matching by @stof in https://github.com/Behat/Gherkin/pull/219
|
||||||
|
- Remove symfony phpunit bridge by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/220
|
||||||
|
- Ignore the bin folder in archives by @stof in https://github.com/Behat/Gherkin/pull/226
|
||||||
|
- Cast table node exceptions into ParserExceptions when throwing by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/216
|
||||||
|
- Cucumber changelog in PRs and using correct hash by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/225
|
||||||
|
- Support alternative docstrings format (```) by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/214
|
||||||
|
- Fix DocBlocks (Boolean -> bool) by @simonhammes in https://github.com/Behat/Gherkin/pull/237
|
||||||
|
- Tag parsing by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/215
|
||||||
|
- Remove test - cucumber added an example with Rule which is not supported by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/239
|
||||||
|
- Add PHP 8.1 support by @javer in https://github.com/Behat/Gherkin/pull/242
|
||||||
|
- Fix main branch alias version by @mvorisek in https://github.com/Behat/Gherkin/pull/244
|
||||||
|
|
||||||
|
# 4.8.0 / 2021-02-04
|
||||||
|
|
||||||
|
- Drop support for PHP before version 7.2
|
||||||
|
|
||||||
|
# 4.7.3 / 2021-02-04
|
||||||
|
|
||||||
|
- Refactored comments parsing to avoid Maximum function nesting level errors
|
||||||
|
|
||||||
|
# 4.7.2 / 2021-02-03
|
||||||
|
|
||||||
|
- Issue where Scenario Outline title was not populated into Examples
|
||||||
|
- Updated translations from cucumber 16.0.0
|
||||||
|
|
||||||
|
# 4.7.1 / 2021-01-26
|
||||||
|
|
||||||
|
- Issue parsing comments before scenarios when following an Examples table
|
||||||
|
|
||||||
|
# 4.7.0 / 2021-01-24
|
||||||
|
|
||||||
|
- Provides better messages for TableNode construct errors
|
||||||
|
- Now allows single character steps
|
||||||
|
- Supports multiple Example Tables with tags
|
||||||
|
|
||||||
|
# 4.6.2 / 2020-03-17
|
||||||
|
|
||||||
|
- Fixed issues due to incorrect cache key
|
||||||
|
|
||||||
|
# 4.6.1 / 2020-02-27
|
||||||
|
|
||||||
|
- Fix AZ translations
|
||||||
|
- Correctly filter features, now that the base path is correctly set
|
||||||
|
|
||||||
|
# 4.6.0 / 2019-01-16
|
||||||
|
|
||||||
|
- Updated translations (including 'Example' as synonym for 'Scenario' in `en`)
|
||||||
|
|
||||||
|
# 4.5.1 / 2017-08-30
|
||||||
|
|
||||||
|
- Fix regression in `PathsFilter`
|
||||||
|
|
||||||
|
# 4.5.0 / 2017-08-30
|
||||||
|
|
||||||
|
- Sync i18n with Cucumber Gherkin
|
||||||
|
- Drop support for HHVM tests on Travis
|
||||||
|
- Add `TableNode::fromList()` method (thanks @TravisCarden)
|
||||||
|
- Add `ExampleNode::getOutlineTitle()` method (thanks @duxet)
|
||||||
|
- Use realpath, so the feature receives the cwd prefixed (thanks @glennunipro)
|
||||||
|
- Explicitly handle non-two-dimensional arrays in TableNode (thanks @TravisCarden)
|
||||||
|
- Fix to line/linefilter scenario runs which take relative paths to files (thanks @generalconsensus)
|
||||||
|
|
||||||
|
# 4.4.5 / 2016-10-30
|
||||||
|
|
||||||
|
- Fix partial paths matching in `PathsFilter`
|
||||||
|
|
||||||
|
# 4.4.4 / 2016-09-18
|
||||||
|
|
||||||
|
- Provide clearer exception for non-writeable cache directories
|
||||||
|
|
||||||
|
# 4.4.3 / 2016-09-18
|
||||||
|
|
||||||
|
- Ensure we reset tags between features
|
||||||
|
|
||||||
|
# 4.4.2 / 2016-09-03
|
||||||
|
|
||||||
|
- Sync 18n with gherkin 3
|
||||||
|
|
||||||
|
# 4.4.1 / 2015-12-30
|
||||||
|
|
||||||
|
- Ensure keywords are trimmed when syncing translations
|
||||||
|
- Sync 18n with cucumber
|
||||||
|
|
||||||
|
# 4.4.0 / 2015-09-19
|
||||||
|
|
||||||
|
- Added validation enforcing that all rows of a `TableNode` have the same number of columns
|
||||||
|
- Added `TableNode::getColumn` to get a column from the table
|
||||||
|
- Sync 18n with cucumber
|
||||||
|
|
||||||
|
# 4.3.0 / 2014-06-06
|
||||||
|
|
||||||
|
- Added `setFilters(array)` method to `Gherkin` class
|
||||||
|
- Added `NarrativeFilter` for non-english `RoleFilter` lovers
|
||||||
|
|
||||||
|
# 4.2.1 / 2014-06-06
|
||||||
|
|
||||||
|
- Fix parsing of features without line feed at the end
|
||||||
|
|
||||||
|
# 4.2.0 / 2014-05-27
|
||||||
|
|
||||||
|
- Added `getKeyword()` and `getKeywordType()` methods to `StepNode`, deprecated `getType()`.
|
||||||
|
Thanks to @kibao
|
||||||
|
|
||||||
|
# 4.1.3 / 2014-05-25
|
||||||
|
|
||||||
|
- Properly handle tables with rows terminating in whitespace
|
||||||
|
|
||||||
|
# 4.1.2 / 2014-05-14
|
||||||
|
|
||||||
|
- Handle case where Gherkin cache is broken
|
||||||
|
|
||||||
|
# 4.1.1 / 2014-05-05
|
||||||
|
|
||||||
|
- Fixed the compatibility with PHP 5.6-beta by avoiding to use the broken PHP array function
|
||||||
|
- The YamlFileLoader no longer extend from ArrayLoader but from AbstractFileLoader
|
||||||
|
|
||||||
|
# 4.1.0 / 2014-04-20
|
||||||
|
|
||||||
|
- Fixed scenario tag filtering
|
||||||
|
- Do not allow multiple multiline step arguments
|
||||||
|
- Sync 18n with cucumber
|
||||||
|
|
||||||
|
# 4.0.0 / 2014-01-05
|
||||||
|
|
||||||
|
- Changed the behavior when no loader can be found for the resource. Instead of throwing an exception, the
|
||||||
|
Gherkin class now returns an empty array.
|
||||||
|
|
||||||
|
# 3.1.3 / 2014-01-04
|
||||||
|
|
||||||
|
- Dropped the dependency on the Symfony Finder by using SPL iterators directly
|
||||||
|
- Added testing on HHVM on Travis. HHVM is officially supported (previous release was actually already compatible)
|
||||||
|
|
||||||
|
# 3.1.2 / 2014-01-01
|
||||||
|
|
||||||
|
- All paths passed to PathsFilter are converted using realpath
|
||||||
|
|
||||||
|
# 3.1.1 / 2013-12-31
|
||||||
|
|
||||||
|
- Add `ComplexFilterInterace` that has complex behavior for scenarios and requires to pass
|
||||||
|
feature too
|
||||||
|
- `TagFilter` is an instance of a `ComplexFilterInterace` now
|
||||||
|
|
||||||
|
# 3.1.0 / 2013-12-31
|
||||||
|
|
||||||
|
- Example node is a scenario
|
||||||
|
- Nodes do not have uprefs (memory usage fix)
|
||||||
|
- Scenario filters do not depend on feature nodes
|
||||||
|
|
||||||
|
# 3.0.5 / 2014-01-01
|
||||||
|
|
||||||
|
- All paths passed to PathsFilter are converted using realpath
|
||||||
|
|
||||||
|
# 3.0.4 / 2013-12-31
|
||||||
|
|
||||||
|
- TableNode is now traversable using foreach
|
||||||
|
- All possibly thrown exceptions implement Gherkin\Exception interface
|
||||||
|
- Sync i18n with cucumber
|
||||||
|
|
||||||
|
# 3.0.3 / 2013-09-15
|
||||||
|
|
||||||
|
- Extend ExampleNode with additional methods
|
||||||
|
|
||||||
|
# 3.0.2 / 2013-09-14
|
||||||
|
|
||||||
|
- Extract `KeywordNodeInterface` and `ScenarioLikeInterface`
|
||||||
|
- Add `getIndex()` methods to scenarios, outlines, steps and examples
|
||||||
|
- Throw proper exception for fractured node tree
|
||||||
|
|
||||||
|
# 3.0.1 / 2013-09-14
|
||||||
|
|
||||||
|
- Use versioned subfolder in FileCache
|
||||||
|
|
||||||
|
# 3.0.0 / 2013-09-14
|
||||||
|
|
||||||
|
- A lot of optimizations in Parser and Lexer
|
||||||
|
- Node tree is now immutable by nature (no setters)
|
||||||
|
- Example nodes are now part of the node tree. They are lazily generated by Outline node
|
||||||
|
- Sync with latest cucumber i18n
|
||||||
|
|
||||||
|
# 2.3.4 / 2013-08-11
|
||||||
|
|
||||||
|
- Fix leaks in memory cache
|
||||||
|
|
||||||
|
# 2.3.3 / 2013-08-11
|
||||||
|
|
||||||
|
- Fix encoding bug introduced with previous release
|
||||||
|
- Sync i18n with cucumber
|
||||||
|
|
||||||
|
# 2.3.2 / 2013-08-11
|
||||||
|
|
||||||
|
- Explicitly use utf8 encoding
|
||||||
|
|
||||||
|
# 2.3.1 / 2013-08-10
|
||||||
|
|
||||||
|
- Support `an` prefix with RoleFilter
|
||||||
|
|
||||||
|
# 2.3.0 / 2013-08-04
|
||||||
|
|
||||||
|
- Add RoleFilter
|
||||||
|
- Add PathsFilter
|
||||||
|
- Add MemoryCache
|
||||||
|
|
||||||
|
# 2.2.9 / 2013-03-02
|
||||||
|
|
||||||
|
- Fix dependency version requirement
|
||||||
|
|
||||||
|
# 2.2.8 / 2013-03-02
|
||||||
|
|
||||||
|
- Features filtering behavior change. Now emptified (by filtering) features
|
||||||
|
that do not match filter themselves are removed from resultset.
|
||||||
|
- Small potential bug fix in TableNode
|
||||||
|
|
||||||
|
# 2.2.7 / 2013-01-27
|
||||||
|
|
||||||
|
- Fixed bug in i18n syncing script
|
||||||
|
- Resynced Gherkin i18n
|
||||||
|
|
||||||
|
# 2.2.6 / 2013-01-26
|
||||||
|
|
||||||
|
- Support long row hashes in tables ([see](https://github.com/Behat/Gherkin/issues/40))
|
||||||
|
- Synced Gherkin i18n
|
||||||
|
|
||||||
|
# 2.2.5 / 2012-09-26
|
||||||
|
|
||||||
|
- Fixed issue with loading empty features
|
||||||
|
- Synced Gherkin i18n
|
||||||
|
|
||||||
|
# 2.2.4 / 2012-08-03
|
||||||
|
|
||||||
|
- Fixed exception message for "no loader found"
|
||||||
|
|
||||||
|
# 2.2.3 / 2012-08-03
|
||||||
|
|
||||||
|
- Fixed minor loader bug with empty base path
|
||||||
|
- Synced Gherkin i18n
|
||||||
|
|
||||||
|
# 2.2.2 / 2012-07-01
|
||||||
|
|
||||||
|
- Added ability to filter outline scenarios by line and range filters
|
||||||
|
- Synced Gherkin i18n
|
||||||
|
- Refactored table parser to read row line numbers too
|
||||||
|
|
||||||
|
# 2.2.1 / 2012-05-04
|
||||||
|
|
||||||
|
- Fixed StepNode `getLanguage()` and `getFile()`
|
||||||
|
|
||||||
|
# 2.2.0 / 2012-05-03
|
||||||
|
|
||||||
|
- Features freeze after parsing
|
||||||
|
- Implemented GherkinDumper (@Halleck45)
|
||||||
|
- Synced i18n with Cucumber
|
||||||
|
- Updated inline documentation
|
||||||
|
|
||||||
|
# 2.1.1 / 2012-03-09
|
||||||
|
|
||||||
|
- Fixed caching bug, where `isFresh()` always returned false
|
||||||
|
|
||||||
|
# 2.1.0 / 2012-03-09
|
||||||
|
|
||||||
|
- Added parser caching layer
|
||||||
|
- Added support for table delimiter escaping (use `\|` for that)
|
||||||
|
- Added LineRangeFilter (thanks @headrevision)
|
||||||
|
- Synced i18n dictionary with cucumber/gherkin
|
||||||
|
|
||||||
|
# 2.0.2 / 2012-02-04
|
||||||
|
|
||||||
|
- Synced i18n dictionary with cucumber/gherkin
|
||||||
|
|
||||||
|
# 2.0.1 / 2012-01-26
|
||||||
|
|
||||||
|
- Fixed issue about parsing features without indentation
|
||||||
|
|
||||||
|
# 2.0.0 / 2012-01-19
|
||||||
|
|
||||||
|
- Background titles support
|
||||||
|
- Correct parsing of titles/descriptions (hirarchy lexing)
|
||||||
|
- Migration to the cucumber/gherkin i18n dictionary
|
||||||
|
- Speed optimizations
|
||||||
|
- Refactored KeywordsDumper
|
||||||
|
- New loaders
|
||||||
|
- Bugfixes
|
||||||
|
|
||||||
|
# 1.1.4 / 2012-01-08
|
||||||
|
|
||||||
|
- Read feature description even if it looks like a step
|
||||||
|
|
||||||
|
# 1.1.3 / 2011-12-14
|
||||||
|
|
||||||
|
- Removed file loading routines from Parser (fixes `is_file()` issue on some systems - thanks
|
||||||
|
@flodocteurklein)
|
||||||
|
|
||||||
|
# 1.1.2 / 2011-12-01
|
||||||
|
|
||||||
|
- Updated spanish trasnaltion (@anbotero)
|
||||||
|
- Integration with Composer and Travis CI
|
||||||
|
|
||||||
|
# 1.1.1 / 2011-07-29
|
||||||
|
|
||||||
|
- Updated pt language step types (@danielcsgomes)
|
||||||
|
- Updated vendors
|
||||||
|
|
||||||
|
# 1.1.0 / 2011-07-16
|
||||||
|
|
||||||
|
- Return all tags, including inherited in `Scenario::getTags()`
|
||||||
|
- New `Feature::getOwnTags()` and `Scenario::getOwnTags()` method added,
|
||||||
|
which returns only own tags
|
||||||
|
|
||||||
|
# 1.0.8 / 2011-06-29
|
||||||
|
|
||||||
|
- Fixed comments parsing.
|
||||||
|
You can’t have comments at the end of a line # like this
|
||||||
|
# But you can still have comments at the beginning of a line
|
||||||
|
|
||||||
|
# 1.0.7 / 2011-06-28
|
||||||
|
|
||||||
|
- Added `getRaw()` method to PyStringNode
|
||||||
|
- Updated vendors
|
||||||
|
|
||||||
|
# 1.0.6 / 2011-06-17
|
||||||
|
|
||||||
|
- Updated vendors
|
||||||
|
|
||||||
|
# 1.0.5 / 2011-06-10
|
||||||
|
|
||||||
|
- Fixed bug, introduced with 1.0.4 - hash in PyStrings
|
||||||
|
|
||||||
|
# 1.0.4 / 2011-06-10
|
||||||
|
|
||||||
|
- Fixed inability to comment pystrings
|
||||||
|
|
||||||
|
# 1.0.3 / 2011-04-21
|
||||||
|
|
||||||
|
- Fixed introduced with 1.0.2 pystring parsing bug
|
||||||
|
|
||||||
|
# 1.0.2 / 2011-04-18
|
||||||
|
|
||||||
|
- Fixed bugs in text with comments parsing
|
||||||
|
|
||||||
|
# 1.0.1 / 2011-04-01
|
||||||
|
|
||||||
|
- Updated vendors
|
||||||
|
|
||||||
|
# 1.0.0 / 2011-03-08
|
||||||
|
|
||||||
|
- Updated vendors
|
||||||
|
|
||||||
|
# 1.0.0RC2 / 2011-02-25
|
||||||
|
|
||||||
|
- Windows support
|
||||||
|
- Missing phpunit config
|
||||||
|
|
||||||
|
# 1.0.0RC1 / 2011-02-15
|
||||||
|
|
||||||
|
- Huge optimizations to Lexer & Parser
|
||||||
|
- Additional loaders (Yaml, Array, Directory)
|
||||||
|
- Filters (Tag, Name, Line)
|
||||||
|
- Code refactoring
|
||||||
|
- Nodes optimizations
|
||||||
|
- Additional tests for exceptions and translations
|
||||||
|
- Keywords dumper
|
||||||
|
|
||||||
|
# 0.2.0 / 2011-01-05
|
||||||
|
|
||||||
|
- New Parser & Lexer (based on AST)
|
||||||
|
- New verbose parsing exception handling
|
||||||
|
- New translation mechanics
|
||||||
|
- 47 brand new translations (see i18n)
|
||||||
|
- Full test suite for everything from AST nodes to translations
|
||||||
|
|
||||||
|
[4.14.0]: https://github.com/Behat/Gherkin/compare/v4.13.0...v4.14.0
|
||||||
|
[4.13.0]: https://github.com/Behat/Gherkin/compare/v4.12.0...v4.13.0
|
||||||
|
[4.12.0]: https://github.com/Behat/Gherkin/compare/v4.11.0...v4.12.0
|
||||||
|
[4.11.0]: https://github.com/Behat/Gherkin/compare/v4.10.0...v4.11.0
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
Copyright (c) 2011-2013 Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
|
||||||
|
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.
|
||||||
Vendored
+58
@@ -0,0 +1,58 @@
|
|||||||
|
# Behat Gherkin Parser
|
||||||
|
|
||||||
|
This is the php Gherkin parser for Behat. It comes bundled with more than 40 native languages (see `i18n.php`) support
|
||||||
|
and clean architecture.
|
||||||
|
|
||||||
|
## Useful Links
|
||||||
|
|
||||||
|
- [Behat Site](https://behat.org)
|
||||||
|
- [Note on Patches/Pull Requests](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array(
|
||||||
|
'en' => array(
|
||||||
|
'feature' => 'Feature',
|
||||||
|
'background' => 'Background',
|
||||||
|
'scenario' => 'Scenario',
|
||||||
|
'scenario_outline' => 'Scenario Outline|Scenario Template',
|
||||||
|
'examples' => 'Examples|Scenarios',
|
||||||
|
'given' => 'Given',
|
||||||
|
'when' => 'When',
|
||||||
|
'then' => 'Then',
|
||||||
|
'and' => 'And',
|
||||||
|
'but' => 'But'
|
||||||
|
),
|
||||||
|
'en-pirate' => array(
|
||||||
|
'feature' => 'Ahoy matey!',
|
||||||
|
'background' => 'Yo-ho-ho',
|
||||||
|
'scenario' => 'Heave to',
|
||||||
|
'scenario_outline' => 'Shiver me timbers',
|
||||||
|
'examples' => 'Dead men tell no tales',
|
||||||
|
'given' => 'Gangway!',
|
||||||
|
'when' => 'Blimey!',
|
||||||
|
'then' => 'Let go and haul',
|
||||||
|
'and' => 'Aye',
|
||||||
|
'but' => 'Avast!'
|
||||||
|
)
|
||||||
|
));
|
||||||
|
$lexer = new Behat\Gherkin\Lexer($keywords);
|
||||||
|
$parser = new Behat\Gherkin\Parser($lexer);
|
||||||
|
|
||||||
|
$feature = $parser->parse(file_get_contents('some.feature'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installing Dependencies
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl https://getcomposer.org/installer | php
|
||||||
|
php composer.phar update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
* Konstantin Kudryashov [everzet](http://github.com/everzet) [lead developer]
|
||||||
|
* Other [awesome developers](https://github.com/Behat/Gherkin/graphs/contributors)
|
||||||
Vendored
+95
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"name": "behat/gherkin",
|
||||||
|
"description": "Gherkin DSL parser for PHP",
|
||||||
|
"keywords": ["BDD", "parser", "DSL", "Behat", "Gherkin", "Cucumber"],
|
||||||
|
"homepage": "https://behat.org/",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Konstantin Kudryashov",
|
||||||
|
"email": "ever.zet@gmail.com",
|
||||||
|
"homepage": "https://everzet.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"require": {
|
||||||
|
"php": "8.1.* || 8.2.* || 8.3.* || 8.4.*",
|
||||||
|
"composer-runtime-api": "^2.2"
|
||||||
|
},
|
||||||
|
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/yaml": "^5.4 || ^6.4 || ^7.0",
|
||||||
|
"phpunit/phpunit": "^10.5",
|
||||||
|
"cucumber/gherkin-monorepo": "dev-gherkin-v32.1.1",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.65",
|
||||||
|
"phpstan/phpstan": "^2",
|
||||||
|
"phpstan/extension-installer": "^1",
|
||||||
|
"phpstan/phpstan-phpunit": "^2",
|
||||||
|
"mikey179/vfsstream": "^1.6"
|
||||||
|
},
|
||||||
|
|
||||||
|
"suggest": {
|
||||||
|
"symfony/yaml": "If you want to parse features, represented in YAML files"
|
||||||
|
},
|
||||||
|
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Behat\\Gherkin\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\Behat\\Gherkin\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "package",
|
||||||
|
"package": {
|
||||||
|
"name": "cucumber/gherkin-monorepo",
|
||||||
|
"version": "dev-gherkin-v32.1.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/cucumber/gherkin.git",
|
||||||
|
"reference": "4fad962021cc96b4b006f711527ab07215df2d22"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/cucumber/gherkin/zipball/4fad962021cc96b4b006f711527ab07215df2d22",
|
||||||
|
"reference": "4fad962021cc96b4b006f711527ab07215df2d22"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"scripts": {
|
||||||
|
"lint": [
|
||||||
|
"phpstan analyze --ansi --no-progress --memory-limit=-1",
|
||||||
|
"phpstan analyze bin/update_cucumber --ansi --no-progress --memory-limit=-1",
|
||||||
|
"phpstan analyze bin/update_i18n --ansi --no-progress --memory-limit=-1",
|
||||||
|
"php-cs-fixer check --diff --ansi --show-progress=dots --verbose"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"phpunit --colors=always"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"php-cs-fixer fix --diff --ansi --show-progress=dots"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"config": {
|
||||||
|
"process-timeout": 0,
|
||||||
|
"allow-plugins": {
|
||||||
|
"phpstan/extension-installer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1293
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Cache;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser cache interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface CacheInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks that cache for feature exists and is fresh.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
* @param int $timestamp The last time feature was updated
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFresh($path, $timestamp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads feature cache from path.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
public function read($path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches feature node.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
* @param FeatureNode $feature Feature instance
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function write($path, FeatureNode $feature);
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Cache;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Exception\CacheException;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Composer\InstalledVersions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File cache.
|
||||||
|
* Caches feature into a file.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class FileCache implements CacheInterface
|
||||||
|
{
|
||||||
|
private readonly string $path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as part of the cache directory path to invalidate cache if the installed package version changes.
|
||||||
|
*/
|
||||||
|
private static function getGherkinVersionHash(): string
|
||||||
|
{
|
||||||
|
$version = InstalledVersions::getVersion('behat/gherkin');
|
||||||
|
|
||||||
|
// Composer version strings can contain arbitrary content so hash for filesystem safety
|
||||||
|
return md5($version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes file cache.
|
||||||
|
*
|
||||||
|
* @param string $path path to the folder where to store caches
|
||||||
|
*
|
||||||
|
* @throws CacheException
|
||||||
|
*/
|
||||||
|
public function __construct(string $path)
|
||||||
|
{
|
||||||
|
$this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . self::getGherkinVersionHash();
|
||||||
|
|
||||||
|
if (!is_dir($this->path)) {
|
||||||
|
@mkdir($this->path, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_writeable($this->path)) {
|
||||||
|
throw new CacheException(sprintf('Cache path "%s" is not writeable. Check your filesystem permissions or disable Gherkin file cache.', $this->path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that cache for feature exists and is fresh.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
* @param int $timestamp The last time feature was updated
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFresh($path, $timestamp)
|
||||||
|
{
|
||||||
|
$cachePath = $this->getCachePathFor($path);
|
||||||
|
|
||||||
|
if (!file_exists($cachePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filemtime($cachePath) > $timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads feature cache from path.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*
|
||||||
|
* @throws CacheException
|
||||||
|
*/
|
||||||
|
public function read($path)
|
||||||
|
{
|
||||||
|
$cachePath = $this->getCachePathFor($path);
|
||||||
|
$feature = unserialize(file_get_contents($cachePath));
|
||||||
|
|
||||||
|
if (!$feature instanceof FeatureNode) {
|
||||||
|
throw new CacheException(sprintf('Can not load cache for a feature "%s" from "%s".', $path, $cachePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches feature node.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
* @param FeatureNode $feature Feature instance
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function write($path, FeatureNode $feature)
|
||||||
|
{
|
||||||
|
file_put_contents($this->getCachePathFor($path), serialize($feature));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature cache file path from features path.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getCachePathFor($path)
|
||||||
|
{
|
||||||
|
return $this->path . '/' . md5($path) . '.feature.cache';
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Cache;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory cache.
|
||||||
|
* Caches feature into a memory.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class MemoryCache implements CacheInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, FeatureNode>
|
||||||
|
*/
|
||||||
|
private array $features = [];
|
||||||
|
/**
|
||||||
|
* @var array<string, int>
|
||||||
|
*/
|
||||||
|
private array $timestamps = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that cache for feature exists and is fresh.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
* @param int $timestamp The last time feature was updated
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFresh($path, $timestamp)
|
||||||
|
{
|
||||||
|
if (!isset($this->features[$path])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->timestamps[$path] > $timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads feature cache from path.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
public function read($path)
|
||||||
|
{
|
||||||
|
return $this->features[$path];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches feature node.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
* @param FeatureNode $feature Feature instance
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function write($path, FeatureNode $feature)
|
||||||
|
{
|
||||||
|
$this->features[$path] = $feature;
|
||||||
|
$this->timestamps[$path] = time();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Exception;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache exception.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class CacheException extends RuntimeException implements Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Exception;
|
||||||
|
|
||||||
|
interface Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Exception;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class LexerException extends RuntimeException implements Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Exception;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class NodeException extends RuntimeException implements Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Exception;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class ParserException extends RuntimeException implements Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Exception;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\NodeInterface;
|
||||||
|
|
||||||
|
class UnexpectedParserNodeException extends ParserException
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $expectation,
|
||||||
|
public readonly string|NodeInterface $node,
|
||||||
|
public readonly ?string $sourceFile,
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
sprintf(
|
||||||
|
'Expected %s, but got %s%s',
|
||||||
|
$expectation,
|
||||||
|
is_string($node)
|
||||||
|
? "text: \"{$node}\""
|
||||||
|
: "{$node->getNodeType()} on line: {$node->getLine()}",
|
||||||
|
$sourceFile ? " in file: {$sourceFile}" : ''
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Exception;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Lexer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-import-type TToken from Lexer
|
||||||
|
*/
|
||||||
|
class UnexpectedTaggedNodeException extends ParserException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @phpstan-param TToken $taggedToken
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly array $taggedToken,
|
||||||
|
public readonly ?string $sourceFile,
|
||||||
|
) {
|
||||||
|
$msg = match ($this->taggedToken['type']) {
|
||||||
|
'EOS' => 'Unexpected end of file after tags',
|
||||||
|
default => sprintf(
|
||||||
|
'%s can not be tagged, but it is',
|
||||||
|
$taggedToken['type'],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
parent::__construct(
|
||||||
|
sprintf(
|
||||||
|
'%s on line: %d%s',
|
||||||
|
$msg,
|
||||||
|
$taggedToken['line'],
|
||||||
|
$this->sourceFile ? " in file: {$this->sourceFile}" : '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract filter class.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
abstract class ComplexFilter implements ComplexFilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Filters feature according to the filter.
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
public function filterFeature(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
$scenarios = $feature->getScenarios();
|
||||||
|
$filteredScenarios = array_filter(
|
||||||
|
$scenarios,
|
||||||
|
fn (ScenarioInterface $scenario) => $this->isScenarioMatch($feature, $scenario)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $scenarios === $filteredScenarios ? $feature : $feature->withScenarios($filteredScenarios);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface ComplexFilterInterface extends FeatureFilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if scenario or outline matches specified filter.
|
||||||
|
*
|
||||||
|
* @param FeatureNode $feature Feature node instance
|
||||||
|
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isScenarioMatch(FeatureNode $feature, ScenarioInterface $scenario);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature filter interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface FeatureFilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if Feature matches specified filter.
|
||||||
|
*
|
||||||
|
* @param FeatureNode $feature Feature instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFeatureMatch(FeatureNode $feature);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters feature according to the filter and returns new one.
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
public function filterFeature(FeatureNode $feature);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface FilterInterface extends FeatureFilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if scenario or outline matches specified filter.
|
||||||
|
*
|
||||||
|
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isScenarioMatch(ScenarioInterface $scenario);
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\ExampleTableNode;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\OutlineNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters scenarios by definition line number.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class LineFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $filterLine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes filter.
|
||||||
|
*
|
||||||
|
* @param int|numeric-string $filterLine Line of the scenario to filter on
|
||||||
|
*/
|
||||||
|
public function __construct($filterLine)
|
||||||
|
{
|
||||||
|
$this->filterLine = (int) $filterLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if Feature matches specified filter.
|
||||||
|
*
|
||||||
|
* @param FeatureNode $feature Feature instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFeatureMatch(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
return $this->filterLine === $feature->getLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if scenario or outline matches specified filter.
|
||||||
|
*
|
||||||
|
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||||
|
{
|
||||||
|
if ($this->filterLine === $scenario->getLine()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||||
|
return $this->filterLine === $scenario->getLine()
|
||||||
|
|| in_array($this->filterLine, $scenario->getExampleTable()->getLines());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters feature according to the filter and returns new one.
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
public function filterFeature(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
$scenarios = [];
|
||||||
|
foreach ($feature->getScenarios() as $scenario) {
|
||||||
|
if (!$this->isScenarioMatch($scenario)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||||
|
foreach ($scenario->getExampleTables() as $exampleTable) {
|
||||||
|
$table = $exampleTable->getTable();
|
||||||
|
$lines = array_keys($table);
|
||||||
|
|
||||||
|
if (in_array($this->filterLine, $lines)) {
|
||||||
|
$filteredTable = [$lines[0] => $table[$lines[0]]];
|
||||||
|
|
||||||
|
if ($lines[0] !== $this->filterLine) {
|
||||||
|
$filteredTable[$this->filterLine] = $table[$this->filterLine];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scenario = $scenario->withTables(
|
||||||
|
[new ExampleTableNode($filteredTable, $exampleTable->getKeyword(), $exampleTable->getTags())],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scenarios[] = $scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $feature->withScenarios($scenarios);
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\ExampleTableNode;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\OutlineNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters scenarios by definition line number range.
|
||||||
|
*
|
||||||
|
* @author Fabian Kiss <headrevision@gmail.com>
|
||||||
|
*/
|
||||||
|
class LineRangeFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $filterMinLine;
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $filterMaxLine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes filter.
|
||||||
|
*
|
||||||
|
* @param int|numeric-string $filterMinLine Minimum line of a scenario to filter on
|
||||||
|
* @param int|numeric-string|'*' $filterMaxLine Maximum line of a scenario to filter on
|
||||||
|
*/
|
||||||
|
public function __construct($filterMinLine, $filterMaxLine)
|
||||||
|
{
|
||||||
|
$this->filterMinLine = (int) $filterMinLine;
|
||||||
|
$this->filterMaxLine = $filterMaxLine === '*' ? PHP_INT_MAX : (int) $filterMaxLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if Feature matches specified filter.
|
||||||
|
*
|
||||||
|
* @param FeatureNode $feature Feature instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFeatureMatch(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
return $this->filterMinLine <= $feature->getLine()
|
||||||
|
&& $this->filterMaxLine >= $feature->getLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if scenario or outline matches specified filter.
|
||||||
|
*
|
||||||
|
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||||
|
{
|
||||||
|
if ($this->filterMinLine <= $scenario->getLine() && $this->filterMaxLine >= $scenario->getLine()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||||
|
foreach ($scenario->getExampleTable()->getLines() as $line) {
|
||||||
|
if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters feature according to the filter.
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
public function filterFeature(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
$scenarios = [];
|
||||||
|
foreach ($feature->getScenarios() as $scenario) {
|
||||||
|
if (!$this->isScenarioMatch($scenario)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||||
|
// first accumulate examples and then create scenario
|
||||||
|
$exampleTableNodes = [];
|
||||||
|
|
||||||
|
foreach ($scenario->getExampleTables() as $exampleTable) {
|
||||||
|
$table = $exampleTable->getTable();
|
||||||
|
$lines = array_keys($table);
|
||||||
|
|
||||||
|
$filteredTable = [$lines[0] => $table[$lines[0]]];
|
||||||
|
unset($table[$lines[0]]);
|
||||||
|
|
||||||
|
foreach ($table as $line => $row) {
|
||||||
|
if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) {
|
||||||
|
$filteredTable[$line] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($filteredTable) > 1) {
|
||||||
|
$exampleTableNodes[] = new ExampleTableNode($filteredTable, $exampleTable->getKeyword(), $exampleTable->getTags());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scenario = $scenario->withTables($exampleTableNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scenarios[] = $scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $feature->withScenarios($scenarios);
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters scenarios by feature/scenario name.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class NameFilter extends SimpleFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $filterString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes filter.
|
||||||
|
*
|
||||||
|
* @param string $filterString Name filter string
|
||||||
|
*/
|
||||||
|
public function __construct($filterString)
|
||||||
|
{
|
||||||
|
$this->filterString = trim($filterString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if Feature matches specified filter.
|
||||||
|
*
|
||||||
|
* @param FeatureNode $feature Feature instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFeatureMatch(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
if ($feature->getTitle() === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->filterString[0] === '/') {
|
||||||
|
return (bool) preg_match($this->filterString, $feature->getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_contains($feature->getTitle(), $this->filterString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if scenario or outline matches specified filter.
|
||||||
|
*
|
||||||
|
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||||
|
{
|
||||||
|
if ($scenario->getTitle() === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->filterString[0] === '/' && preg_match($this->filterString, $scenario->getTitle())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($scenario->getTitle(), $this->filterString)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters features by their narrative using regular expression.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class NarrativeFilter extends SimpleFilter
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $regex,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFeatureMatch(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
return (bool) preg_match($this->regex, $feature->getDescription() ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||||
|
{
|
||||||
|
// This filter does not apply to scenarios.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters features by their paths.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class PathsFilter extends SimpleFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $filterPaths = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes filter.
|
||||||
|
*
|
||||||
|
* @param array<array-key, string> $paths List of approved paths
|
||||||
|
*/
|
||||||
|
public function __construct(array $paths)
|
||||||
|
{
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
if (($realpath = realpath($path)) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->filterPaths[] = rtrim($realpath, DIRECTORY_SEPARATOR)
|
||||||
|
. (is_dir($realpath) ? DIRECTORY_SEPARATOR : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFeatureMatch(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
foreach ($this->filterPaths as $path) {
|
||||||
|
if (str_starts_with(realpath($feature->getFile()), $path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||||
|
{
|
||||||
|
// This filter does not apply to scenarios.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters features by their actors role.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class RoleFilter extends SimpleFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes filter.
|
||||||
|
*
|
||||||
|
* @param string $role Approved role wildcard
|
||||||
|
*/
|
||||||
|
public function __construct($role)
|
||||||
|
{
|
||||||
|
$this->pattern = sprintf(
|
||||||
|
'/as an? %s[$\n]/i',
|
||||||
|
strtr(
|
||||||
|
preg_quote($role, '/'),
|
||||||
|
[
|
||||||
|
'\*' => '.*',
|
||||||
|
'\?' => '.',
|
||||||
|
'\[' => '[',
|
||||||
|
'\]' => ']',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFeatureMatch(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
return (bool) preg_match($this->pattern, $feature->getDescription() ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||||
|
{
|
||||||
|
// This filter does not apply to scenarios.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract filter class.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
abstract class SimpleFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Filters feature according to the filter.
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
public function filterFeature(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
if ($this->isFeatureMatch($feature)) {
|
||||||
|
return $feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $feature->withScenarios(
|
||||||
|
array_filter(
|
||||||
|
$feature->getScenarios(),
|
||||||
|
$this->isScenarioMatch(...)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Filter;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\OutlineNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters scenarios by feature/scenario tag.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class TagFilter extends ComplexFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $filterString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes filter.
|
||||||
|
*
|
||||||
|
* @param string $filterString Name filter string
|
||||||
|
*/
|
||||||
|
public function __construct($filterString)
|
||||||
|
{
|
||||||
|
$this->filterString = trim($filterString);
|
||||||
|
|
||||||
|
if (preg_match('/\s/u', $this->filterString)) {
|
||||||
|
trigger_error(
|
||||||
|
'Tags with whitespace are deprecated and may be removed in a future version',
|
||||||
|
E_USER_DEPRECATED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters feature according to the filter.
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
public function filterFeature(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
$scenarios = [];
|
||||||
|
foreach ($feature->getScenarios() as $scenario) {
|
||||||
|
if (!$this->isScenarioMatch($feature, $scenario)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||||
|
$exampleTables = [];
|
||||||
|
|
||||||
|
foreach ($scenario->getExampleTables() as $exampleTable) {
|
||||||
|
if ($this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags(), $exampleTable->getTags()))) {
|
||||||
|
$exampleTables[] = $exampleTable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scenario = $scenario->withTables($exampleTables);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scenarios[] = $scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $feature->withScenarios($scenarios);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if Feature matches specified filter.
|
||||||
|
*
|
||||||
|
* @param FeatureNode $feature Feature instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isFeatureMatch(FeatureNode $feature)
|
||||||
|
{
|
||||||
|
return $this->isTagsMatchCondition($feature->getTags());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if scenario or outline matches specified filter.
|
||||||
|
*
|
||||||
|
* @param FeatureNode $feature Feature node instance
|
||||||
|
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isScenarioMatch(FeatureNode $feature, ScenarioInterface $scenario)
|
||||||
|
{
|
||||||
|
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||||
|
foreach ($scenario->getExampleTables() as $example) {
|
||||||
|
if ($this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags(), $example->getTags()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that node matches condition.
|
||||||
|
*
|
||||||
|
* @param array<array-key, string> $tags
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isTagsMatchCondition($tags)
|
||||||
|
{
|
||||||
|
if ($this->filterString === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (explode('&&', $this->filterString) as $andTags) {
|
||||||
|
$satisfiesComma = false;
|
||||||
|
|
||||||
|
foreach (explode(',', $andTags) as $tag) {
|
||||||
|
$tag = str_replace('@', '', trim($tag));
|
||||||
|
|
||||||
|
if ($tag[0] === '~') {
|
||||||
|
$tag = mb_substr($tag, 1, mb_strlen($tag, 'utf8') - 1, 'utf8');
|
||||||
|
$satisfiesComma = !in_array($tag, $tags, true) || $satisfiesComma;
|
||||||
|
} else {
|
||||||
|
$satisfiesComma = in_array($tag, $tags, true) || $satisfiesComma;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$satisfiesComma) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+156
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Filter\FeatureFilterInterface;
|
||||||
|
use Behat\Gherkin\Filter\LineFilter;
|
||||||
|
use Behat\Gherkin\Filter\LineRangeFilter;
|
||||||
|
use Behat\Gherkin\Loader\FileLoaderInterface;
|
||||||
|
use Behat\Gherkin\Loader\LoaderInterface;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin manager.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class Gherkin
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @deprecated this constant will not be updated for releases after 4.8.0 and will be removed in the next major.
|
||||||
|
* You can use composer's runtime API to get the behat version if you need it. Note that composer's versions will
|
||||||
|
* not always be simple numeric values.
|
||||||
|
*/
|
||||||
|
public const VERSION = '4.8.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<LoaderInterface>
|
||||||
|
*/
|
||||||
|
protected $loaders = [];
|
||||||
|
/**
|
||||||
|
* @var list<FeatureFilterInterface>
|
||||||
|
*/
|
||||||
|
protected $filters = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds loader to manager.
|
||||||
|
*
|
||||||
|
* @param LoaderInterface $loader Feature loader
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addLoader(LoaderInterface $loader)
|
||||||
|
{
|
||||||
|
$this->loaders[] = $loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds filter to manager.
|
||||||
|
*
|
||||||
|
* @param FeatureFilterInterface $filter Feature filter
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addFilter(FeatureFilterInterface $filter)
|
||||||
|
{
|
||||||
|
$this->filters[] = $filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets filters to the parser.
|
||||||
|
*
|
||||||
|
* @param array<array-key, FeatureFilterInterface> $filters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setFilters(array $filters)
|
||||||
|
{
|
||||||
|
$this->filters = [];
|
||||||
|
array_map($this->addFilter(...), $filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets base features path.
|
||||||
|
*
|
||||||
|
* @param string $path Loaders base path
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setBasePath($path)
|
||||||
|
{
|
||||||
|
foreach ($this->loaders as $loader) {
|
||||||
|
if ($loader instanceof FileLoaderInterface) {
|
||||||
|
$loader->setBasePath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads & filters resource with added loaders.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
* @param array<array-key, FeatureFilterInterface> $filters Additional filters
|
||||||
|
*
|
||||||
|
* @return list<FeatureNode>
|
||||||
|
*/
|
||||||
|
public function load($resource, array $filters = [])
|
||||||
|
{
|
||||||
|
$filters = array_merge($this->filters, $filters);
|
||||||
|
|
||||||
|
$matches = [];
|
||||||
|
if (preg_match('/^(.*):(\d+)-(\d+|\*)$/', $resource, $matches)) {
|
||||||
|
$resource = $matches[1];
|
||||||
|
$filters[] = new LineRangeFilter($matches[2], $matches[3]);
|
||||||
|
} elseif (preg_match('/^(.*):(\d+)$/', $resource, $matches)) {
|
||||||
|
$resource = $matches[1];
|
||||||
|
$filters[] = new LineFilter($matches[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$loader = $this->resolveLoader($resource);
|
||||||
|
|
||||||
|
if ($loader === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$features = [];
|
||||||
|
foreach ($loader->load($resource) as $feature) {
|
||||||
|
foreach ($filters as $filter) {
|
||||||
|
$feature = $filter->filterFeature($feature);
|
||||||
|
|
||||||
|
if (!$feature->hasScenarios() && !$filter->isFeatureMatch($feature)) {
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$features[] = $feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $features;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves loader by resource.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return LoaderInterface|null
|
||||||
|
*/
|
||||||
|
public function resolveLoader($resource)
|
||||||
|
{
|
||||||
|
foreach ($this->loaders as $loader) {
|
||||||
|
if ($loader->supports($resource)) {
|
||||||
|
return $loader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Keywords;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array initializable keywords holder.
|
||||||
|
*
|
||||||
|
* $keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array(
|
||||||
|
* 'en' => array(
|
||||||
|
* 'feature' => 'Feature',
|
||||||
|
* 'background' => 'Background',
|
||||||
|
* 'scenario' => 'Scenario',
|
||||||
|
* 'scenario_outline' => 'Scenario Outline|Scenario Template',
|
||||||
|
* 'examples' => 'Examples|Scenarios',
|
||||||
|
* 'given' => 'Given',
|
||||||
|
* 'when' => 'When',
|
||||||
|
* 'then' => 'Then',
|
||||||
|
* 'and' => 'And',
|
||||||
|
* 'but' => 'But'
|
||||||
|
* ),
|
||||||
|
* 'ru' => array(
|
||||||
|
* 'feature' => 'Функционал',
|
||||||
|
* 'background' => 'Предыстория',
|
||||||
|
* 'scenario' => 'Сценарий',
|
||||||
|
* 'scenario_outline' => 'Структура сценария',
|
||||||
|
* 'examples' => 'Примеры',
|
||||||
|
* 'given' => 'Допустим',
|
||||||
|
* 'when' => 'Если',
|
||||||
|
* 'then' => 'То',
|
||||||
|
* 'and' => 'И',
|
||||||
|
* 'but' => 'Но'
|
||||||
|
* )
|
||||||
|
* ));
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* @phpstan-import-type TMultiLanguageKeywords from KeywordsInterface
|
||||||
|
*/
|
||||||
|
class ArrayKeywords implements KeywordsInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private array $keywordString = [];
|
||||||
|
private string $language = 'en';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes holder with keywords.
|
||||||
|
*
|
||||||
|
* @phpstan-param TMultiLanguageKeywords $keywords Keywords array
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $keywords,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets keywords holder language.
|
||||||
|
*
|
||||||
|
* @param string $language Language name
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setLanguage($language)
|
||||||
|
{
|
||||||
|
if (!isset($this->keywords[$language])) {
|
||||||
|
$this->language = 'en';
|
||||||
|
} else {
|
||||||
|
$this->language = $language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Feature keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFeatureKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['feature'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Background keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getBackgroundKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['background'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Scenario keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getScenarioKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['scenario'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Scenario Outline keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getOutlineKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['scenario_outline'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Examples keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getExamplesKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['examples'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Given keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getGivenKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['given'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns When keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getWhenKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['when'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Then keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getThenKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['then'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns And keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAndKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['and'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns But keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getButKeywords()
|
||||||
|
{
|
||||||
|
return $this->keywords[$this->language]['but'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all step keywords (Given, When, Then, And, But).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getStepKeywords()
|
||||||
|
{
|
||||||
|
if (!isset($this->keywordString[$this->language])) {
|
||||||
|
$keywords = array_merge(
|
||||||
|
explode('|', $this->getGivenKeywords()),
|
||||||
|
explode('|', $this->getWhenKeywords()),
|
||||||
|
explode('|', $this->getThenKeywords()),
|
||||||
|
explode('|', $this->getAndKeywords()),
|
||||||
|
explode('|', $this->getButKeywords())
|
||||||
|
);
|
||||||
|
|
||||||
|
usort($keywords, function ($keyword1, $keyword2) {
|
||||||
|
return mb_strlen($keyword2, 'utf8') - mb_strlen($keyword1, 'utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->keywordString[$this->language] = implode('|', $keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->keywordString[$this->language];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Keywords;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File initializable keywords holder.
|
||||||
|
*
|
||||||
|
* $keywords = new Behat\Gherkin\Keywords\CachedArrayKeywords($file);
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class CachedArrayKeywords extends ArrayKeywords
|
||||||
|
{
|
||||||
|
public static function withDefaultKeywords(): self
|
||||||
|
{
|
||||||
|
return new self(__DIR__ . '/../../i18n.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes holder with file.
|
||||||
|
*
|
||||||
|
* @param string $file Cached array path
|
||||||
|
*/
|
||||||
|
public function __construct($file)
|
||||||
|
{
|
||||||
|
parent::__construct(require $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Keywords;
|
||||||
|
|
||||||
|
use Symfony\Component\Yaml\Exception\ParseException;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cucumber-translations reader.
|
||||||
|
*
|
||||||
|
* $keywords = new Behat\Gherkin\Keywords\CucumberKeywords($i18nYmlPath);
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class CucumberKeywords extends ArrayKeywords
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Initializes holder with yaml string OR file.
|
||||||
|
*
|
||||||
|
* @param string $yaml Yaml string or file path
|
||||||
|
*/
|
||||||
|
public function __construct($yaml)
|
||||||
|
{
|
||||||
|
if (!str_contains($yaml, "\n") && is_file($yaml)) {
|
||||||
|
$content = Yaml::parseFile($yaml);
|
||||||
|
} else {
|
||||||
|
$content = Yaml::parse($yaml);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($content)) {
|
||||||
|
throw new ParseException(sprintf('Root element must be an array, but %s found.', get_debug_type($content)));
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Feature keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getGivenKeywords()
|
||||||
|
{
|
||||||
|
return $this->prepareStepString(parent::getGivenKeywords());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns When keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getWhenKeywords()
|
||||||
|
{
|
||||||
|
return $this->prepareStepString(parent::getWhenKeywords());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Then keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getThenKeywords()
|
||||||
|
{
|
||||||
|
return $this->prepareStepString(parent::getThenKeywords());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns And keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAndKeywords()
|
||||||
|
{
|
||||||
|
return $this->prepareStepString(parent::getAndKeywords());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns But keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getButKeywords()
|
||||||
|
{
|
||||||
|
return $this->prepareStepString(parent::getButKeywords());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim *| from the begining of the list.
|
||||||
|
*
|
||||||
|
* @param string $keywordsString Keywords string
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function prepareStepString($keywordsString)
|
||||||
|
{
|
||||||
|
if (str_starts_with($keywordsString, '*|')) {
|
||||||
|
$keywordsString = mb_substr($keywordsString, 2, mb_strlen($keywordsString, 'utf8') - 2, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keywordsString;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Keywords;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin keywords dumper.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class KeywordsDumper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var callable(list<string>, bool): string
|
||||||
|
*/
|
||||||
|
private $keywordsDumper;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly KeywordsInterface $keywords,
|
||||||
|
) {
|
||||||
|
$this->keywordsDumper = [$this, 'dumpKeywords'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets keywords mapper function.
|
||||||
|
*
|
||||||
|
* Callable should accept 2 arguments (array $keywords and bool $isShort)
|
||||||
|
*
|
||||||
|
* @param callable(list<string>, bool): string $mapper Mapper function
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setKeywordsDumperFunction($mapper)
|
||||||
|
{
|
||||||
|
$this->keywordsDumper = $mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults keywords dumper.
|
||||||
|
*
|
||||||
|
* @param list<string> $keywords Keywords list
|
||||||
|
* @param bool $isShort Is short version
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function dumpKeywords(array $keywords, $isShort)
|
||||||
|
{
|
||||||
|
if ($isShort) {
|
||||||
|
return count($keywords) > 1
|
||||||
|
? '(' . implode('|', $keywords) . ')'
|
||||||
|
: $keywords[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keywords[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps keyworded feature into string.
|
||||||
|
*
|
||||||
|
* @param string $language Keywords language
|
||||||
|
* @param bool $short Dump short version
|
||||||
|
* @param bool $excludeAsterisk
|
||||||
|
*
|
||||||
|
* @return string|array String for short version and array of features for extended
|
||||||
|
*
|
||||||
|
* @phpstan-return ($short is true ? string : list<string>)
|
||||||
|
*/
|
||||||
|
public function dump($language, $short = true, $excludeAsterisk = false)
|
||||||
|
{
|
||||||
|
$this->keywords->setLanguage($language);
|
||||||
|
$languageComment = '';
|
||||||
|
if ($language !== 'en') {
|
||||||
|
$languageComment = "# language: $language\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$keywords = explode('|', $this->keywords->getFeatureKeywords());
|
||||||
|
|
||||||
|
if ($short) {
|
||||||
|
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||||
|
|
||||||
|
return trim($languageComment . $this->dumpFeature($keywords, $short, $excludeAsterisk));
|
||||||
|
}
|
||||||
|
|
||||||
|
$features = [];
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
$keyword = call_user_func($this->keywordsDumper, [$keyword], $short);
|
||||||
|
$features[] = trim($languageComment . $this->dumpFeature($keyword, $short, $excludeAsterisk));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $features;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps feature example.
|
||||||
|
*
|
||||||
|
* @param string $keyword Item keyword
|
||||||
|
* @param bool $short Dump short version?
|
||||||
|
* @param bool $excludeAsterisk
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function dumpFeature($keyword, $short = true, $excludeAsterisk = false)
|
||||||
|
{
|
||||||
|
$dump = <<<GHERKIN
|
||||||
|
{$keyword}: Internal operations
|
||||||
|
In order to stay secret
|
||||||
|
As a secret organization
|
||||||
|
We need to be able to erase past agents' memory
|
||||||
|
|
||||||
|
|
||||||
|
GHERKIN;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
$keywords = explode('|', $this->keywords->getBackgroundKeywords());
|
||||||
|
if ($short) {
|
||||||
|
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||||
|
$dump .= $this->dumpBackground($keywords, $short, $excludeAsterisk);
|
||||||
|
} else {
|
||||||
|
$keyword = call_user_func($this->keywordsDumper, [$keywords[0]], $short);
|
||||||
|
$dump .= $this->dumpBackground($keyword, $short, $excludeAsterisk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scenario
|
||||||
|
$keywords = explode('|', $this->keywords->getScenarioKeywords());
|
||||||
|
if ($short) {
|
||||||
|
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||||
|
$dump .= $this->dumpScenario($keywords, $short, $excludeAsterisk);
|
||||||
|
} else {
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
$keyword = call_user_func($this->keywordsDumper, [$keyword], $short);
|
||||||
|
$dump .= $this->dumpScenario($keyword, $short, $excludeAsterisk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outline
|
||||||
|
$keywords = explode('|', $this->keywords->getOutlineKeywords());
|
||||||
|
if ($short) {
|
||||||
|
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||||
|
$dump .= $this->dumpOutline($keywords, $short, $excludeAsterisk);
|
||||||
|
} else {
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
$keyword = call_user_func($this->keywordsDumper, [$keyword], $short);
|
||||||
|
$dump .= $this->dumpOutline($keyword, $short, $excludeAsterisk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dump;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps background example.
|
||||||
|
*
|
||||||
|
* @param string $keyword Item keyword
|
||||||
|
* @param bool $short Dump short version?
|
||||||
|
* @param bool $excludeAsterisk
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function dumpBackground($keyword, $short = true, $excludeAsterisk = false)
|
||||||
|
{
|
||||||
|
$dump = <<<GHERKIN
|
||||||
|
{$keyword}:
|
||||||
|
|
||||||
|
GHERKIN;
|
||||||
|
|
||||||
|
// Given
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getGivenKeywords(),
|
||||||
|
'there is agent A',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// And
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getAndKeywords(),
|
||||||
|
'there is agent B',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
return $dump . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps scenario example.
|
||||||
|
*
|
||||||
|
* @param string $keyword Item keyword
|
||||||
|
* @param bool $short Dump short version?
|
||||||
|
* @param bool $excludeAsterisk
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function dumpScenario($keyword, $short = true, $excludeAsterisk = false)
|
||||||
|
{
|
||||||
|
$dump = <<<GHERKIN
|
||||||
|
{$keyword}: Erasing agent memory
|
||||||
|
|
||||||
|
GHERKIN;
|
||||||
|
|
||||||
|
// Given
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getGivenKeywords(),
|
||||||
|
'there is agent J',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// And
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getAndKeywords(),
|
||||||
|
'there is agent K',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// When
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getWhenKeywords(),
|
||||||
|
'I erase agent K\'s memory',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getThenKeywords(),
|
||||||
|
'there should be agent J',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// But
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getButKeywords(),
|
||||||
|
'there should not be agent K',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
return $dump . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps outline example.
|
||||||
|
*
|
||||||
|
* @param string $keyword Item keyword
|
||||||
|
* @param bool $short Dump short version?
|
||||||
|
* @param bool $excludeAsterisk
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function dumpOutline($keyword, $short = true, $excludeAsterisk = false)
|
||||||
|
{
|
||||||
|
$dump = <<<GHERKIN
|
||||||
|
{$keyword}: Erasing other agents' memory
|
||||||
|
|
||||||
|
GHERKIN;
|
||||||
|
|
||||||
|
// Given
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getGivenKeywords(),
|
||||||
|
'there is agent <agent1>',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// And
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getAndKeywords(),
|
||||||
|
'there is agent <agent2>',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// When
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getWhenKeywords(),
|
||||||
|
'I erase agent <agent2>\'s memory',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getThenKeywords(),
|
||||||
|
'there should be agent <agent1>',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
// But
|
||||||
|
$dump .= $this->dumpStep(
|
||||||
|
$this->keywords->getButKeywords(),
|
||||||
|
'there should not be agent <agent2>',
|
||||||
|
$short,
|
||||||
|
$excludeAsterisk
|
||||||
|
);
|
||||||
|
|
||||||
|
$keywords = explode('|', $this->keywords->getExamplesKeywords());
|
||||||
|
if ($short) {
|
||||||
|
$keyword = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||||
|
} else {
|
||||||
|
$keyword = call_user_func($this->keywordsDumper, [$keywords[0]], $short);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dump .= <<<GHERKIN
|
||||||
|
|
||||||
|
{$keyword}:
|
||||||
|
| agent1 | agent2 |
|
||||||
|
| D | M |
|
||||||
|
|
||||||
|
GHERKIN;
|
||||||
|
|
||||||
|
return $dump . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps step example.
|
||||||
|
*
|
||||||
|
* @param string $keywords Item keyword
|
||||||
|
* @param string $text Step text
|
||||||
|
* @param bool $short Dump short version?
|
||||||
|
* @param bool $excludeAsterisk
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function dumpStep($keywords, $text, $short = true, $excludeAsterisk = false)
|
||||||
|
{
|
||||||
|
$dump = '';
|
||||||
|
|
||||||
|
$keywords = explode('|', $keywords);
|
||||||
|
if ($short) {
|
||||||
|
$keywords = array_map(
|
||||||
|
function ($keyword) {
|
||||||
|
return str_replace('<', '', $keyword);
|
||||||
|
},
|
||||||
|
$keywords
|
||||||
|
);
|
||||||
|
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
|
||||||
|
$dump .= <<<GHERKIN
|
||||||
|
{$keywords} {$text}
|
||||||
|
|
||||||
|
GHERKIN;
|
||||||
|
} else {
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
if ($excludeAsterisk && $keyword === '*') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$indent = ' ';
|
||||||
|
if (str_contains($keyword, '<')) {
|
||||||
|
$keyword = mb_substr($keyword, 0, -1, 'utf8');
|
||||||
|
$indent = '';
|
||||||
|
}
|
||||||
|
$keyword = call_user_func($this->keywordsDumper, [$keyword], $short);
|
||||||
|
$dump .= <<<GHERKIN
|
||||||
|
{$keyword}{$indent}{$text}
|
||||||
|
|
||||||
|
GHERKIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dump;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Keywords;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keywords holder interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* @phpstan-type TLanguage string
|
||||||
|
* @phpstan-type TKeywordsArray array{feature: string, background: string, scenario: string, scenario_outline: string, examples: string, given: string, when: string, then: string, and: string, but: string}
|
||||||
|
* @phpstan-type TMultiLanguageKeywords array<TLanguage, TKeywordsArray>
|
||||||
|
*/
|
||||||
|
interface KeywordsInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Sets keywords holder language.
|
||||||
|
*
|
||||||
|
* @param string $language Language name
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setLanguage($language);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Feature keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFeatureKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Background keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getBackgroundKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Scenario keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getScenarioKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Scenario Outline keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getOutlineKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Examples keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getExamplesKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Given keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getGivenKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns When keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getWhenKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Then keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getThenKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns And keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAndKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns But keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getButKeywords();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all step keywords (separated by "|").
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getStepKeywords();
|
||||||
|
}
|
||||||
Vendored
+767
@@ -0,0 +1,767 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Exception\LexerException;
|
||||||
|
use Behat\Gherkin\Keywords\KeywordsInterface;
|
||||||
|
use LogicException;
|
||||||
|
|
||||||
|
use function assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin lexer.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* @phpstan-type TToken array{type: string, line: int, value: string|null, deferred: bool, tags?: list<string>, keyword?: string, keyword_type?: string, text?: string, indent?: int, columns?: list<string>}
|
||||||
|
*/
|
||||||
|
class Lexer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Splits a string around | char, only if it's not preceded by an odd number of \.
|
||||||
|
*
|
||||||
|
* @see https://github.com/cucumber/gherkin/blob/679a87e21263699c15ea635159c6cda60f64af3b/php/src/StringGherkinLine.php#L14
|
||||||
|
*/
|
||||||
|
private const CELL_PATTERN = '/(?<!\\\\)(?:\\\\{2})*\K\\|/u';
|
||||||
|
private string $language;
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private array $lines;
|
||||||
|
private int $linesCount;
|
||||||
|
private string $line;
|
||||||
|
private ?string $trimmedLine = null;
|
||||||
|
private int $lineNumber;
|
||||||
|
private bool $eos;
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private array $keywordsCache = [];
|
||||||
|
/**
|
||||||
|
* @var array<string, list<string>>
|
||||||
|
*/
|
||||||
|
private array $stepKeywordTypesCache = [];
|
||||||
|
/**
|
||||||
|
* @phpstan-var list<TToken>
|
||||||
|
*/
|
||||||
|
private array $deferredObjects = [];
|
||||||
|
private int $deferredObjectsCount = 0;
|
||||||
|
/**
|
||||||
|
* @phpstan-var TToken|null
|
||||||
|
*/
|
||||||
|
private ?array $stashedToken = null;
|
||||||
|
private bool $inPyString = false;
|
||||||
|
private int $pyStringSwallow = 0;
|
||||||
|
private bool $allowLanguageTag = true;
|
||||||
|
private bool $allowFeature = true;
|
||||||
|
private bool $allowMultilineArguments = false;
|
||||||
|
private bool $allowSteps = false;
|
||||||
|
private ?string $pyStringDelimiter = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly KeywordsInterface $keywords,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets lexer input.
|
||||||
|
*
|
||||||
|
* @param string $input Input string
|
||||||
|
* @param string $language Language name
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws LexerException
|
||||||
|
*/
|
||||||
|
public function analyse($input, $language = 'en')
|
||||||
|
{
|
||||||
|
// try to detect unsupported encoding
|
||||||
|
if (mb_detect_encoding($input, 'UTF-8', true) !== 'UTF-8') {
|
||||||
|
throw new LexerException('Feature file is not in UTF8 encoding');
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = strtr($input, ["\r\n" => "\n", "\r" => "\n"]);
|
||||||
|
|
||||||
|
$this->lines = explode("\n", $input);
|
||||||
|
$this->linesCount = count($this->lines);
|
||||||
|
$this->line = $this->lines[0];
|
||||||
|
$this->lineNumber = 1;
|
||||||
|
$this->trimmedLine = null;
|
||||||
|
$this->eos = false;
|
||||||
|
|
||||||
|
$this->deferredObjects = [];
|
||||||
|
$this->deferredObjectsCount = 0;
|
||||||
|
$this->stashedToken = null;
|
||||||
|
$this->inPyString = false;
|
||||||
|
$this->pyStringSwallow = 0;
|
||||||
|
|
||||||
|
$this->allowLanguageTag = true;
|
||||||
|
$this->allowFeature = true;
|
||||||
|
$this->allowMultilineArguments = false;
|
||||||
|
$this->allowSteps = false;
|
||||||
|
|
||||||
|
$this->setLanguage($language);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setLanguage(string $language): void
|
||||||
|
{
|
||||||
|
if (($this->stashedToken !== null) || ($this->deferredObjects !== [])) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
// It is not possible to trigger this condition using the public interface of this class.
|
||||||
|
// It may be possible if the end-user has extended the Lexer with custom functionality.
|
||||||
|
throw new LogicException(
|
||||||
|
<<<'STRING'
|
||||||
|
Cannot set gherkin language due to unexpected Lexer state.
|
||||||
|
|
||||||
|
Please open an issue at https://github.com/Behat/Gherkin with a copy of the current
|
||||||
|
feature file. If you are using a Lexer or Parser class that extends the ones provided
|
||||||
|
in behat/gherkin, please also provide details of these.
|
||||||
|
STRING,
|
||||||
|
);
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->keywords->setLanguage($this->language = $language);
|
||||||
|
$this->keywordsCache = [];
|
||||||
|
$this->stepKeywordTypesCache = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current lexer language.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getLanguage()
|
||||||
|
{
|
||||||
|
return $this->language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns next token or previously stashed one.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken
|
||||||
|
*/
|
||||||
|
public function getAdvancedToken()
|
||||||
|
{
|
||||||
|
return $this->getStashedToken() ?: $this->getNextToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defers token.
|
||||||
|
*
|
||||||
|
* @phpstan-param TToken $token Token to defer
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function deferToken(array $token)
|
||||||
|
{
|
||||||
|
$token['deferred'] = true;
|
||||||
|
$this->deferredObjects[] = $token;
|
||||||
|
++$this->deferredObjectsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predicts the upcoming token without passing over it.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken
|
||||||
|
*/
|
||||||
|
public function predictToken()
|
||||||
|
{
|
||||||
|
return $this->stashedToken ??= $this->getNextToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips over the currently-predicted token, if any.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function skipPredictedToken()
|
||||||
|
{
|
||||||
|
$this->stashedToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs token with specified parameters.
|
||||||
|
*
|
||||||
|
* @param string $type Token type
|
||||||
|
* @param string|null $value Token value
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken
|
||||||
|
*/
|
||||||
|
public function takeToken($type, $value = null)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => $type,
|
||||||
|
'line' => $this->lineNumber,
|
||||||
|
'value' => $value ?: null,
|
||||||
|
'deferred' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes line from input & increments line counter.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function consumeLine()
|
||||||
|
{
|
||||||
|
++$this->lineNumber;
|
||||||
|
|
||||||
|
if (($this->lineNumber - 1) === $this->linesCount) {
|
||||||
|
$this->eos = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line = $this->lines[$this->lineNumber - 1];
|
||||||
|
$this->trimmedLine = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes first part of line from input without incrementing the line number.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function consumeLineUntil(int $trimmedOffset)
|
||||||
|
{
|
||||||
|
$this->line = mb_substr(ltrim($this->line), $trimmedOffset, null, 'utf-8');
|
||||||
|
$this->trimmedLine = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns trimmed version of line.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getTrimmedLine()
|
||||||
|
{
|
||||||
|
return $this->trimmedLine ??= trim($this->line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns stashed token or null if hasn't.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function getStashedToken()
|
||||||
|
{
|
||||||
|
$stashedToken = $this->stashedToken;
|
||||||
|
$this->stashedToken = null;
|
||||||
|
|
||||||
|
return $stashedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns deferred token or null if hasn't.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function getDeferredToken()
|
||||||
|
{
|
||||||
|
if (!$this->deferredObjectsCount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
--$this->deferredObjectsCount;
|
||||||
|
|
||||||
|
return array_shift($this->deferredObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns next token from input.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken
|
||||||
|
*/
|
||||||
|
protected function getNextToken()
|
||||||
|
{
|
||||||
|
return $this->getDeferredToken()
|
||||||
|
?: $this->scanEOS()
|
||||||
|
?: $this->scanLanguage()
|
||||||
|
?: $this->scanComment()
|
||||||
|
?: $this->scanPyStringOp()
|
||||||
|
?: $this->scanPyStringContent()
|
||||||
|
?: $this->scanStep()
|
||||||
|
?: $this->scanScenario()
|
||||||
|
?: $this->scanBackground()
|
||||||
|
?: $this->scanOutline()
|
||||||
|
?: $this->scanExamples()
|
||||||
|
?: $this->scanFeature()
|
||||||
|
?: $this->scanTags()
|
||||||
|
?: $this->scanTableRow()
|
||||||
|
?: $this->scanNewline()
|
||||||
|
?: $this->scanText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans for token with specified regex.
|
||||||
|
*
|
||||||
|
* @param string $regex Regular expression
|
||||||
|
* @param string $type Expected token type
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanInput($regex, $type)
|
||||||
|
{
|
||||||
|
if (!preg_match($regex, $this->line, $matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->takeToken($type, $matches[1]);
|
||||||
|
$this->consumeLine();
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans for token with specified keywords.
|
||||||
|
*
|
||||||
|
* @param string $keywords Keywords (separated by "|")
|
||||||
|
* @param string $type Expected token type
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanInputForKeywords($keywords, $type)
|
||||||
|
{
|
||||||
|
if (!preg_match('/^(\s*)(' . $keywords . '):\s*(.*)/u', $this->line, $matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->takeToken($type, $matches[3]);
|
||||||
|
$token['keyword'] = $matches[2];
|
||||||
|
$token['indent'] = mb_strlen($matches[1], 'utf8');
|
||||||
|
|
||||||
|
$this->consumeLine();
|
||||||
|
|
||||||
|
// turn off language searching and feature detection
|
||||||
|
if ($type === 'Feature') {
|
||||||
|
$this->allowFeature = false;
|
||||||
|
$this->allowLanguageTag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn off PyString and Table searching
|
||||||
|
if ($type === 'Feature' || $type === 'Scenario' || $type === 'Outline') {
|
||||||
|
$this->allowMultilineArguments = false;
|
||||||
|
} elseif ($type === 'Examples') {
|
||||||
|
$this->allowMultilineArguments = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn on steps searching
|
||||||
|
if ($type === 'Scenario' || $type === 'Background' || $type === 'Outline') {
|
||||||
|
$this->allowSteps = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans EOS from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanEOS()
|
||||||
|
{
|
||||||
|
if (!$this->eos) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->takeToken('EOS');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns keywords for provided type.
|
||||||
|
*
|
||||||
|
* @param string $type Keyword type
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getKeywords($type)
|
||||||
|
{
|
||||||
|
if (!isset($this->keywordsCache[$type])) {
|
||||||
|
$getter = 'get' . $type . 'Keywords';
|
||||||
|
$keywords = $this->keywords->$getter();
|
||||||
|
|
||||||
|
if ($type === 'Step') {
|
||||||
|
$padded = [];
|
||||||
|
foreach (explode('|', $keywords) as $keyword) {
|
||||||
|
$padded[] = str_contains($keyword, '<')
|
||||||
|
? preg_quote(mb_substr($keyword, 0, -1, 'utf8'), '/') . '\s*'
|
||||||
|
: preg_quote($keyword, '/') . '\s+';
|
||||||
|
}
|
||||||
|
|
||||||
|
$keywords = implode('|', $padded);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->keywordsCache[$type] = $keywords;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->keywordsCache[$type];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Feature from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanFeature()
|
||||||
|
{
|
||||||
|
if (!$this->allowFeature) {
|
||||||
|
// The Feature: tag is only allowed once in a file, later in the file it may be part of a description node
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->scanInputForKeywords($this->getKeywords('Feature'), 'Feature');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Background from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanBackground()
|
||||||
|
{
|
||||||
|
return $this->scanInputForKeywords($this->getKeywords('Background'), 'Background');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Scenario from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanScenario()
|
||||||
|
{
|
||||||
|
return $this->scanInputForKeywords($this->getKeywords('Scenario'), 'Scenario');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Scenario Outline from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanOutline()
|
||||||
|
{
|
||||||
|
return $this->scanInputForKeywords($this->getKeywords('Outline'), 'Outline');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Scenario Outline Examples from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanExamples()
|
||||||
|
{
|
||||||
|
return $this->scanInputForKeywords($this->getKeywords('Examples'), 'Examples');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Step from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanStep()
|
||||||
|
{
|
||||||
|
if (!$this->allowSteps) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keywords = $this->getKeywords('Step');
|
||||||
|
if (!preg_match('/^\s*(' . $keywords . ')([^\s].*)/u', $this->line, $matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyword = trim($matches[1]);
|
||||||
|
$token = $this->takeToken('Step', $keyword);
|
||||||
|
$token['keyword_type'] = $this->getStepKeywordType($keyword);
|
||||||
|
$token['text'] = $matches[2];
|
||||||
|
|
||||||
|
$this->consumeLine();
|
||||||
|
$this->allowMultilineArguments = true;
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans PyString from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanPyStringOp()
|
||||||
|
{
|
||||||
|
if (!$this->allowMultilineArguments) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^\s*(?<delimiter>"""|```)/u', $this->line, $matches, PREG_OFFSET_CAPTURE)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
['delimiter' => [0 => $delimiter, 1 => $indent]] = $matches;
|
||||||
|
|
||||||
|
if ($this->inPyString) {
|
||||||
|
if ($this->pyStringDelimiter !== $delimiter) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$this->pyStringDelimiter = null;
|
||||||
|
} else {
|
||||||
|
$this->pyStringDelimiter = $delimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->inPyString = !$this->inPyString;
|
||||||
|
$token = $this->takeToken('PyStringOp');
|
||||||
|
$this->pyStringSwallow = $indent;
|
||||||
|
|
||||||
|
$this->consumeLine();
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans PyString content.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanPyStringContent()
|
||||||
|
{
|
||||||
|
if (!$this->inPyString) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->scanText();
|
||||||
|
// swallow trailing spaces
|
||||||
|
$token['value'] = preg_replace('/^\s{0,' . $this->pyStringSwallow . '}/u', '', $token['value'] ?? '');
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Table Row from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanTableRow()
|
||||||
|
{
|
||||||
|
if (!$this->allowMultilineArguments) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = $this->getTrimmedLine();
|
||||||
|
if (!str_starts_with($line, '|')) {
|
||||||
|
// Strictly speaking, a table row only has to begin with a pipe - content to the right
|
||||||
|
// of the final pipe will be ignored after we split the cells.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawColumns = preg_split(self::CELL_PATTERN, $line);
|
||||||
|
assert($rawColumns !== false);
|
||||||
|
|
||||||
|
// Safely remove elements before the first and last separators
|
||||||
|
array_shift($rawColumns);
|
||||||
|
array_pop($rawColumns);
|
||||||
|
|
||||||
|
$token = $this->takeToken('TableRow');
|
||||||
|
$columns = array_map(function ($column) {
|
||||||
|
return trim(str_replace(['\\|', '\\\\'], ['|', '\\'], $column));
|
||||||
|
}, $rawColumns);
|
||||||
|
$token['columns'] = $columns;
|
||||||
|
|
||||||
|
$this->consumeLine();
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Tags from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanTags()
|
||||||
|
{
|
||||||
|
$line = $this->getTrimmedLine();
|
||||||
|
|
||||||
|
if ($line === '' || !str_starts_with($line, '@')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^(?<line>.*)\s+#.*$/', $line, $matches)) {
|
||||||
|
['line' => $line] = $matches;
|
||||||
|
$this->consumeLineUntil(mb_strlen($line, 'utf-8'));
|
||||||
|
} else {
|
||||||
|
$this->consumeLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->takeToken('Tag');
|
||||||
|
$tags = explode('@', mb_substr($line, 1, mb_strlen($line, 'utf8') - 1, 'utf8'));
|
||||||
|
$tags = array_map('trim', $tags);
|
||||||
|
$token['tags'] = $tags;
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Language specifier from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanLanguage()
|
||||||
|
{
|
||||||
|
if (!$this->allowLanguageTag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->inPyString) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!str_starts_with(ltrim($this->line), '#')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->scanInput('/^\s*#\s*language:\s*([\w_\-]+)\s*$/', 'Language');
|
||||||
|
|
||||||
|
if ($token) {
|
||||||
|
\assert(\is_string($token['value']));
|
||||||
|
$this->allowLanguageTag = false;
|
||||||
|
$this->setLanguage($token['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Comment from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanComment()
|
||||||
|
{
|
||||||
|
if ($this->inPyString) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = $this->getTrimmedLine();
|
||||||
|
if (!str_starts_with($line, '#')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->takeToken('Comment', $line);
|
||||||
|
$this->consumeLine();
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans Newline from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function scanNewline()
|
||||||
|
{
|
||||||
|
if ($this->getTrimmedLine() !== '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->takeToken('Newline');
|
||||||
|
$this->consumeLine();
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans text from input & returns it if found.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken
|
||||||
|
*/
|
||||||
|
protected function scanText()
|
||||||
|
{
|
||||||
|
$token = $this->takeToken('Text', $this->line);
|
||||||
|
$this->consumeLine();
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns step type keyword (Given, When, Then, etc.).
|
||||||
|
*
|
||||||
|
* @param string $native Step keyword in provided language
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getStepKeywordType($native)
|
||||||
|
{
|
||||||
|
// Consider "*" as a AND keyword so that it is normalized to the previous step type
|
||||||
|
if ($native === '*') {
|
||||||
|
return 'And';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->stepKeywordTypesCache)) {
|
||||||
|
$this->stepKeywordTypesCache = [
|
||||||
|
'Given' => explode('|', $this->keywords->getGivenKeywords()),
|
||||||
|
'When' => explode('|', $this->keywords->getWhenKeywords()),
|
||||||
|
'Then' => explode('|', $this->keywords->getThenKeywords()),
|
||||||
|
'And' => explode('|', $this->keywords->getAndKeywords()),
|
||||||
|
'But' => explode('|', $this->keywords->getButKeywords()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->stepKeywordTypesCache as $type => $keywords) {
|
||||||
|
if (in_array($native, $keywords, true) || in_array($native . '<', $keywords, true)) {
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Given';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Loader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract filesystem loader.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
abstract class AbstractFileLoader implements FileLoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
protected $basePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets base features path.
|
||||||
|
*
|
||||||
|
* @param string $path Base loader path
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setBasePath($path)
|
||||||
|
{
|
||||||
|
$this->basePath = realpath($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds relative path for provided absolute (relative to base features path).
|
||||||
|
*
|
||||||
|
* @param string $path Absolute path
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function findRelativePath($path)
|
||||||
|
{
|
||||||
|
if ($this->basePath !== null) {
|
||||||
|
return strtr($path, [$this->basePath . DIRECTORY_SEPARATOR => '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds absolute path for provided relative (relative to base features path).
|
||||||
|
*
|
||||||
|
* @param string $path Relative path
|
||||||
|
*
|
||||||
|
* @return false|string
|
||||||
|
*/
|
||||||
|
protected function findAbsolutePath($path)
|
||||||
|
{
|
||||||
|
if (is_file($path) || is_dir($path)) {
|
||||||
|
return realpath($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->basePath === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_file($this->basePath . DIRECTORY_SEPARATOR . $path)
|
||||||
|
|| is_dir($this->basePath . DIRECTORY_SEPARATOR . $path)) {
|
||||||
|
return realpath($this->basePath . DIRECTORY_SEPARATOR . $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
final protected function getAbsolutePath(string $path): string
|
||||||
|
{
|
||||||
|
$resolvedPath = $this->findAbsolutePath($path);
|
||||||
|
if ($resolvedPath === false) {
|
||||||
|
throw new \RuntimeException("Unable to locate absolute path of \"$path\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolvedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Loader;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\BackgroundNode;
|
||||||
|
use Behat\Gherkin\Node\ExampleTableNode;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\OutlineNode;
|
||||||
|
use Behat\Gherkin\Node\PyStringNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioNode;
|
||||||
|
use Behat\Gherkin\Node\StepNode;
|
||||||
|
use Behat\Gherkin\Node\TableNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From-array loader.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* @phpstan-type TFeatureHash array{title?: string|null, description?: string|null, tags?: list<string>, keyword?: string, language?: string, line?: int, background?: TBackgroundHash|null, scenarios?: array<int, TScenarioHash|TOutlineHash>}
|
||||||
|
* @phpstan-type TBackgroundHash array{title?: string|null, keyword?: string, line?: int, steps?: array<int, TStepHash>}
|
||||||
|
* @phpstan-type TScenarioHash array{type?: 'scenario', title?: string|null, tags?: list<string>, keyword?: string, line?: int, steps?: array<int, TStepHash>}
|
||||||
|
* @phpstan-type TOutlineHash array{type: 'outline', title?: string|null, tags?: list<string>, keyword?: string, line?: int, steps?: array<int, TStepHash>, examples?: TExampleTableHash|array<array-key, TExampleHash>}
|
||||||
|
* @phpstan-type TExampleHash array{table: TExampleTableHash, tags?: list<string>}|TExampleTableHash
|
||||||
|
* @phpstan-type TExampleTableHash array<int<1, max>, list<string>>
|
||||||
|
* @phpstan-type TStepHash array{keyword_type?: string, type?: string, text: string, keyword?: string, line?: int, arguments?: array<array-key, TArgumentHash>}
|
||||||
|
* @phpstan-type TArgumentHash array{type: 'table', rows: TTableHash}|TPySstringHash
|
||||||
|
* @phpstan-type TTableHash array<int, list<string|int>>
|
||||||
|
* @phpstan-type TPySstringHash array{type: 'pystring', line?: int, text: string}
|
||||||
|
* @phpstan-type TArrayResource array{feature: TFeatureHash}|array{features: array<int, TFeatureHash>}
|
||||||
|
*/
|
||||||
|
class ArrayLoader implements LoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if current loader supports provided resource.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function supports($resource)
|
||||||
|
{
|
||||||
|
return is_array($resource) && (isset($resource['features']) || isset($resource['feature']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads features from provided resource.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
*
|
||||||
|
* @phpstan-param TArrayResource $resource
|
||||||
|
*
|
||||||
|
* @return list<FeatureNode>
|
||||||
|
*/
|
||||||
|
public function load($resource)
|
||||||
|
{
|
||||||
|
$features = [];
|
||||||
|
|
||||||
|
if (isset($resource['features'])) {
|
||||||
|
foreach ($resource['features'] as $iterator => $hash) {
|
||||||
|
$feature = $this->loadFeatureHash($hash, $iterator);
|
||||||
|
$features[] = $feature;
|
||||||
|
}
|
||||||
|
} elseif (isset($resource['feature'])) {
|
||||||
|
$feature = $this->loadFeatureHash($resource['feature']);
|
||||||
|
$features[] = $feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $features;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads feature from provided feature hash.
|
||||||
|
*
|
||||||
|
* @phpstan-param TFeatureHash $hash
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*/
|
||||||
|
protected function loadFeatureHash(array $hash, int $line = 0)
|
||||||
|
{
|
||||||
|
$hash = array_merge(
|
||||||
|
[
|
||||||
|
'title' => null,
|
||||||
|
'description' => null,
|
||||||
|
'tags' => [],
|
||||||
|
'keyword' => 'Feature',
|
||||||
|
'language' => 'en',
|
||||||
|
'line' => $line,
|
||||||
|
'scenarios' => [],
|
||||||
|
],
|
||||||
|
$hash
|
||||||
|
);
|
||||||
|
$background = isset($hash['background']) ? $this->loadBackgroundHash($hash['background']) : null;
|
||||||
|
|
||||||
|
$scenarios = [];
|
||||||
|
foreach ((array) $hash['scenarios'] as $scenarioIterator => $scenarioHash) {
|
||||||
|
if (isset($scenarioHash['type']) && $scenarioHash['type'] === 'outline') {
|
||||||
|
$scenarios[] = $this->loadOutlineHash($scenarioHash, $scenarioIterator);
|
||||||
|
} else {
|
||||||
|
$scenarios[] = $this->loadScenarioHash($scenarioHash, $scenarioIterator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FeatureNode($hash['title'], $hash['description'], $hash['tags'], $background, $scenarios, $hash['keyword'], $hash['language'], null, $hash['line']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads background from provided hash.
|
||||||
|
*
|
||||||
|
* @phpstan-param TBackgroundHash $hash
|
||||||
|
*
|
||||||
|
* @return BackgroundNode
|
||||||
|
*/
|
||||||
|
protected function loadBackgroundHash(array $hash)
|
||||||
|
{
|
||||||
|
$hash = array_merge(
|
||||||
|
[
|
||||||
|
'title' => null,
|
||||||
|
'keyword' => 'Background',
|
||||||
|
'line' => 0,
|
||||||
|
'steps' => [],
|
||||||
|
],
|
||||||
|
$hash
|
||||||
|
);
|
||||||
|
|
||||||
|
$steps = $this->loadStepsHash($hash['steps']);
|
||||||
|
|
||||||
|
return new BackgroundNode($hash['title'], $steps, $hash['keyword'], $hash['line']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads scenario from provided scenario hash.
|
||||||
|
*
|
||||||
|
* @phpstan-param TScenarioHash $hash
|
||||||
|
*
|
||||||
|
* @return ScenarioNode
|
||||||
|
*/
|
||||||
|
protected function loadScenarioHash(array $hash, int $line = 0)
|
||||||
|
{
|
||||||
|
$hash = array_merge(
|
||||||
|
[
|
||||||
|
'title' => null,
|
||||||
|
'tags' => [],
|
||||||
|
'keyword' => 'Scenario',
|
||||||
|
'line' => $line,
|
||||||
|
'steps' => [],
|
||||||
|
],
|
||||||
|
$hash
|
||||||
|
);
|
||||||
|
|
||||||
|
$steps = $this->loadStepsHash($hash['steps']);
|
||||||
|
|
||||||
|
return new ScenarioNode($hash['title'], $hash['tags'], $steps, $hash['keyword'], $hash['line']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads outline from provided outline hash.
|
||||||
|
*
|
||||||
|
* @phpstan-param TOutlineHash $hash
|
||||||
|
*
|
||||||
|
* @return OutlineNode
|
||||||
|
*/
|
||||||
|
protected function loadOutlineHash(array $hash, int $line = 0)
|
||||||
|
{
|
||||||
|
$hash = array_merge(
|
||||||
|
[
|
||||||
|
'title' => null,
|
||||||
|
'tags' => [],
|
||||||
|
'keyword' => 'Scenario Outline',
|
||||||
|
'line' => $line,
|
||||||
|
'steps' => [],
|
||||||
|
'examples' => [],
|
||||||
|
],
|
||||||
|
$hash
|
||||||
|
);
|
||||||
|
|
||||||
|
$steps = $this->loadStepsHash($hash['steps']);
|
||||||
|
|
||||||
|
if (isset($hash['examples']['keyword'])) {
|
||||||
|
$examplesKeyword = $hash['examples']['keyword'];
|
||||||
|
unset($hash['examples']['keyword']);
|
||||||
|
} else {
|
||||||
|
$examplesKeyword = 'Examples';
|
||||||
|
}
|
||||||
|
|
||||||
|
$examples = $this->loadExamplesHash($hash['examples'], $examplesKeyword);
|
||||||
|
|
||||||
|
return new OutlineNode($hash['title'], $hash['tags'], $steps, $examples, $hash['keyword'], $hash['line']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads steps from provided hash.
|
||||||
|
*
|
||||||
|
* @phpstan-param array<int, TStepHash> $hash
|
||||||
|
*
|
||||||
|
* @return list<StepNode>
|
||||||
|
*/
|
||||||
|
private function loadStepsHash(array $hash)
|
||||||
|
{
|
||||||
|
$steps = [];
|
||||||
|
foreach ($hash as $stepIterator => $stepHash) {
|
||||||
|
$steps[] = $this->loadStepHash($stepHash, $stepIterator);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads step from provided hash.
|
||||||
|
*
|
||||||
|
* @phpstan-param TStepHash $hash
|
||||||
|
*
|
||||||
|
* @return StepNode
|
||||||
|
*/
|
||||||
|
protected function loadStepHash(array $hash, int $line = 0)
|
||||||
|
{
|
||||||
|
$hash = array_merge(
|
||||||
|
[
|
||||||
|
'keyword_type' => 'Given',
|
||||||
|
'type' => 'Given',
|
||||||
|
'text' => null,
|
||||||
|
'keyword' => 'Scenario',
|
||||||
|
'line' => $line,
|
||||||
|
'arguments' => [],
|
||||||
|
],
|
||||||
|
$hash
|
||||||
|
);
|
||||||
|
|
||||||
|
$arguments = [];
|
||||||
|
foreach ($hash['arguments'] as $argumentHash) {
|
||||||
|
if ($argumentHash['type'] === 'table') {
|
||||||
|
$arguments[] = $this->loadTableHash($argumentHash['rows']);
|
||||||
|
} elseif ($argumentHash['type'] === 'pystring') {
|
||||||
|
$arguments[] = $this->loadPyStringHash($argumentHash, $hash['line'] + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepNode($hash['type'], $hash['text'], $arguments, $hash['line'], $hash['keyword_type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads table from provided hash.
|
||||||
|
*
|
||||||
|
* @phpstan-param TTableHash $hash
|
||||||
|
*
|
||||||
|
* @return TableNode
|
||||||
|
*/
|
||||||
|
protected function loadTableHash(array $hash)
|
||||||
|
{
|
||||||
|
return new TableNode($hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads PyString from provided hash.
|
||||||
|
*
|
||||||
|
* @phpstan-param TPySstringHash $hash
|
||||||
|
*
|
||||||
|
* @return PyStringNode
|
||||||
|
*/
|
||||||
|
protected function loadPyStringHash(array $hash, int $line = 0)
|
||||||
|
{
|
||||||
|
$line = $hash['line'] ?? $line;
|
||||||
|
|
||||||
|
$strings = [];
|
||||||
|
foreach (explode("\n", $hash['text']) as $string) {
|
||||||
|
$strings[] = $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PyStringNode($strings, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes cases when examples are in the form of array of arrays
|
||||||
|
* OR in the form of array of objects.
|
||||||
|
*
|
||||||
|
* @phpstan-param TExampleHash|array<array-key, TExampleHash> $examplesHash
|
||||||
|
*
|
||||||
|
* @return list<ExampleTableNode>
|
||||||
|
*/
|
||||||
|
private function loadExamplesHash(array $examplesHash, string $examplesKeyword): array
|
||||||
|
{
|
||||||
|
if (!isset($examplesHash[0])) {
|
||||||
|
// examples as a single table - create a list with the one element
|
||||||
|
return [new ExampleTableNode($examplesHash, $examplesKeyword)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$examples = [];
|
||||||
|
|
||||||
|
foreach ($examplesHash as $exampleHash) {
|
||||||
|
if (isset($exampleHash['table'])) {
|
||||||
|
// we have examples as objects, hence there could be tags
|
||||||
|
$exHashTags = $exampleHash['tags'] ?? [];
|
||||||
|
$examples[] = new ExampleTableNode($exampleHash['table'], $examplesKeyword, $exHashTags);
|
||||||
|
} else {
|
||||||
|
// we have examples as arrays
|
||||||
|
$examples[] = new ExampleTableNode($exampleHash, $examplesKeyword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $examples;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Loader;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Exception\NodeException;
|
||||||
|
use Behat\Gherkin\Node\ArgumentInterface;
|
||||||
|
use Behat\Gherkin\Node\BackgroundNode;
|
||||||
|
use Behat\Gherkin\Node\ExampleTableNode;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\OutlineNode;
|
||||||
|
use Behat\Gherkin\Node\PyStringNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
use Behat\Gherkin\Node\ScenarioNode;
|
||||||
|
use Behat\Gherkin\Node\StepNode;
|
||||||
|
use Behat\Gherkin\Node\TableNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a feature from cucumber's messages JSON format.
|
||||||
|
*
|
||||||
|
* Lines in the ndjson file are expected to match the Cucumber Messages JSON schema defined at https://github.com/cucumber/messages/tree/main/jsonschema
|
||||||
|
*
|
||||||
|
* @phpstan-type TLocation array{line: int, column?: int}
|
||||||
|
* @phpstan-type TBackground array{location: TLocation, keyword: string, name: string, description: string, steps: list<TStep>, id: string}
|
||||||
|
* @phpstan-type TComment array{location: TLocation, text: string}
|
||||||
|
* @phpstan-type TDataTable array{location: TLocation, rows: list<TTableRow>}
|
||||||
|
* @phpstan-type TDocString array{location: TLocation, content: string, delimiter: string, mediaType?: string}
|
||||||
|
* @phpstan-type TExamples array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, tableHeader?: TTableRow, tableBody: list<TTableRow>, id: string}
|
||||||
|
* @phpstan-type TFeature array{location: TLocation, tags: list<TTag>, language: string, keyword: string, name: string, description: string, children: list<TFeatureChild>}
|
||||||
|
* @phpstan-type TFeatureChild array{background?: TBackground, scenario?: TScenario, rule?: TRule}
|
||||||
|
* @phpstan-type TRule array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, children: list<TRuleChild>, id: string}
|
||||||
|
* @phpstan-type TRuleChild array{background?: TBackground, scenario?: TScenario}
|
||||||
|
* @phpstan-type TScenario array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, steps: list<TStep>, examples: list<TExamples>, id: string}
|
||||||
|
* @phpstan-type TStep array{location: TLocation, keyword: string, keywordType?: 'Unknown'|'Context'|'Action'|'Outcome'|'Conjunction', text: string, docString?: TDocString, dataTable?: TDataTable, id: string}
|
||||||
|
* @phpstan-type TTableCell array{location: TLocation, value: string}
|
||||||
|
* @phpstan-type TTableRow array{location: TLocation, cells: list<TTableCell>, id: string}
|
||||||
|
* @phpstan-type TTag array{location: TLocation, name: string, id: string}
|
||||||
|
* @phpstan-type TGherkinDocument array{uri?: string, feature?: TFeature, comments: list<TComment>}
|
||||||
|
* // We only care about the gherkinDocument messages for our use case, so this does not describe the envelope fully
|
||||||
|
* @phpstan-type TEnvelope array{gherkinDocument?: TGherkinDocument, ...}
|
||||||
|
*/
|
||||||
|
class CucumberNDJsonAstLoader implements LoaderInterface
|
||||||
|
{
|
||||||
|
public function supports($resource)
|
||||||
|
{
|
||||||
|
return is_string($resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load($resource)
|
||||||
|
{
|
||||||
|
return array_values(
|
||||||
|
array_filter(
|
||||||
|
array_map(
|
||||||
|
static function ($line) use ($resource) {
|
||||||
|
// As we load data from the official Cucumber project, we assume the data matches the JSON schema.
|
||||||
|
return self::getFeature(json_decode($line, true, 512, \JSON_THROW_ON_ERROR), $resource);
|
||||||
|
},
|
||||||
|
file($resource)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-param TEnvelope $json
|
||||||
|
*/
|
||||||
|
private static function getFeature(array $json, string $filePath): ?FeatureNode
|
||||||
|
{
|
||||||
|
if (!isset($json['gherkinDocument']['feature'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$featureJson = $json['gherkinDocument']['feature'];
|
||||||
|
|
||||||
|
return new FeatureNode(
|
||||||
|
$featureJson['name'],
|
||||||
|
$featureJson['description'],
|
||||||
|
self::getTags($featureJson),
|
||||||
|
self::getBackground($featureJson),
|
||||||
|
self::getScenarios($featureJson),
|
||||||
|
$featureJson['keyword'],
|
||||||
|
$featureJson['language'],
|
||||||
|
preg_replace('/(?<=\\.feature).*$/', '', $filePath),
|
||||||
|
$featureJson['location']['line']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-param array{tags: list<TTag>, ...} $json
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private static function getTags(array $json): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
static fn (array $tag) => preg_replace('/^@/', '', $tag['name']),
|
||||||
|
$json['tags']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-param TFeature $json
|
||||||
|
*
|
||||||
|
* @return list<ScenarioInterface>
|
||||||
|
*/
|
||||||
|
private static function getScenarios(array $json): array
|
||||||
|
{
|
||||||
|
return array_values(
|
||||||
|
array_map(
|
||||||
|
static function ($child) {
|
||||||
|
$tables = self::getTables($child['scenario']['examples']);
|
||||||
|
|
||||||
|
if ($tables) {
|
||||||
|
return new OutlineNode(
|
||||||
|
$child['scenario']['name'],
|
||||||
|
self::getTags($child['scenario']),
|
||||||
|
self::getSteps($child['scenario']['steps']),
|
||||||
|
$tables,
|
||||||
|
$child['scenario']['keyword'],
|
||||||
|
$child['scenario']['location']['line']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ScenarioNode(
|
||||||
|
$child['scenario']['name'],
|
||||||
|
self::getTags($child['scenario']),
|
||||||
|
self::getSteps($child['scenario']['steps']),
|
||||||
|
$child['scenario']['keyword'],
|
||||||
|
$child['scenario']['location']['line']
|
||||||
|
);
|
||||||
|
},
|
||||||
|
array_filter(
|
||||||
|
$json['children'],
|
||||||
|
static function ($child) {
|
||||||
|
return isset($child['scenario']);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-param TFeature $json
|
||||||
|
*/
|
||||||
|
private static function getBackground(array $json): ?BackgroundNode
|
||||||
|
{
|
||||||
|
$backgrounds = array_filter(
|
||||||
|
$json['children'],
|
||||||
|
static fn ($child) => isset($child['background']),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (count($backgrounds) !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$background = array_shift($backgrounds);
|
||||||
|
|
||||||
|
return new BackgroundNode(
|
||||||
|
$background['background']['name'],
|
||||||
|
self::getSteps($background['background']['steps']),
|
||||||
|
$background['background']['keyword'],
|
||||||
|
$background['background']['location']['line']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-param list<TStep> $items
|
||||||
|
*
|
||||||
|
* @return list<StepNode>
|
||||||
|
*/
|
||||||
|
private static function getSteps(array $items): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
static fn (array $item) => new StepNode(
|
||||||
|
trim($item['keyword']),
|
||||||
|
$item['text'],
|
||||||
|
self::getStepArguments($item),
|
||||||
|
$item['location']['line'],
|
||||||
|
trim($item['keyword'])
|
||||||
|
),
|
||||||
|
$items
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-param TStep $step
|
||||||
|
*
|
||||||
|
* @return list<ArgumentInterface>
|
||||||
|
*/
|
||||||
|
private static function getStepArguments(array $step): array
|
||||||
|
{
|
||||||
|
$args = [];
|
||||||
|
|
||||||
|
if (isset($step['docString'])) {
|
||||||
|
$args[] = new PyStringNode(
|
||||||
|
explode("\n", $step['docString']['content']),
|
||||||
|
$step['docString']['location']['line'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($step['dataTable'])) {
|
||||||
|
$table = [];
|
||||||
|
foreach ($step['dataTable']['rows'] as $row) {
|
||||||
|
$table[$row['location']['line']] = array_column($row['cells'], 'value');
|
||||||
|
}
|
||||||
|
$args[] = new TableNode($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-param list<TExamples> $items
|
||||||
|
*
|
||||||
|
* @return list<ExampleTableNode>
|
||||||
|
*/
|
||||||
|
private static function getTables(array $items): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
static function ($tableJson): ExampleTableNode {
|
||||||
|
$headerRow = $tableJson['tableHeader'] ?? null;
|
||||||
|
$tableBody = $tableJson['tableBody'];
|
||||||
|
|
||||||
|
if ($headerRow === null && ($tableBody !== [])) {
|
||||||
|
throw new NodeException(
|
||||||
|
sprintf(
|
||||||
|
'Table header is required when a table body is provided for the example on line %s.',
|
||||||
|
$tableJson['location']['line'],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = [];
|
||||||
|
if ($headerRow !== null) {
|
||||||
|
$table[$headerRow['location']['line']] = array_column($headerRow['cells'], 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($tableBody as $bodyRow) {
|
||||||
|
$table[$bodyRow['location']['line']] = array_column($bodyRow['cells'], 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ExampleTableNode(
|
||||||
|
$table,
|
||||||
|
$tableJson['keyword'],
|
||||||
|
self::getTags($tableJson)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$items
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Loader;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Gherkin;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory contents loader.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class DirectoryLoader extends AbstractFileLoader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Gherkin
|
||||||
|
*/
|
||||||
|
protected $gherkin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes loader.
|
||||||
|
*
|
||||||
|
* @param Gherkin $gherkin Gherkin manager
|
||||||
|
*/
|
||||||
|
public function __construct(Gherkin $gherkin)
|
||||||
|
{
|
||||||
|
$this->gherkin = $gherkin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if current loader supports provided resource.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function supports($resource)
|
||||||
|
{
|
||||||
|
return is_string($resource)
|
||||||
|
&& ($path = $this->findAbsolutePath($resource)) !== false
|
||||||
|
&& is_dir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads features from provided resource.
|
||||||
|
*
|
||||||
|
* @param string $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return list<FeatureNode>
|
||||||
|
*/
|
||||||
|
public function load($resource)
|
||||||
|
{
|
||||||
|
$path = $this->getAbsolutePath($resource);
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||||
|
);
|
||||||
|
$paths = array_map(strval(...), iterator_to_array($iterator));
|
||||||
|
uasort($paths, strnatcasecmp(...));
|
||||||
|
|
||||||
|
$features = [];
|
||||||
|
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
$path = (string) $path;
|
||||||
|
$loader = $this->gherkin->resolveLoader($path);
|
||||||
|
|
||||||
|
if ($loader !== null) {
|
||||||
|
array_push($features, ...$loader->load($path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $features;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Loader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File Loader interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface FileLoaderInterface extends LoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Sets base features path.
|
||||||
|
*
|
||||||
|
* @param string $path Base loader path
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setBasePath($path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Loader;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Cache\CacheInterface;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin *.feature files loader.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class GherkinFileLoader extends AbstractFileLoader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Parser
|
||||||
|
*/
|
||||||
|
protected $parser;
|
||||||
|
/**
|
||||||
|
* @var CacheInterface|null
|
||||||
|
*/
|
||||||
|
protected $cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes loader.
|
||||||
|
*
|
||||||
|
* @param Parser $parser Parser
|
||||||
|
* @param CacheInterface|null $cache Cache layer
|
||||||
|
*/
|
||||||
|
public function __construct(Parser $parser, ?CacheInterface $cache = null)
|
||||||
|
{
|
||||||
|
$this->parser = $parser;
|
||||||
|
$this->cache = $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets cache layer.
|
||||||
|
*
|
||||||
|
* @param CacheInterface $cache Cache layer
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setCache(CacheInterface $cache)
|
||||||
|
{
|
||||||
|
$this->cache = $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if current loader supports provided resource.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function supports($resource)
|
||||||
|
{
|
||||||
|
return is_string($resource)
|
||||||
|
&& ($path = $this->findAbsolutePath($resource)) !== false
|
||||||
|
&& is_file($path)
|
||||||
|
&& pathinfo($path, PATHINFO_EXTENSION) === 'feature';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads features from provided resource.
|
||||||
|
*
|
||||||
|
* @param string $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return list<FeatureNode>
|
||||||
|
*/
|
||||||
|
public function load($resource)
|
||||||
|
{
|
||||||
|
$path = $this->getAbsolutePath($resource);
|
||||||
|
if ($this->cache) {
|
||||||
|
if ($this->cache->isFresh($path, filemtime($path))) {
|
||||||
|
$feature = $this->cache->read($path);
|
||||||
|
} elseif (null !== $feature = $this->parseFeature($path)) {
|
||||||
|
$this->cache->write($path, $feature);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$feature = $this->parseFeature($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $feature !== null ? [$feature] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses feature at provided absolute path.
|
||||||
|
*
|
||||||
|
* @param string $path Feature path
|
||||||
|
*
|
||||||
|
* @return FeatureNode|null
|
||||||
|
*/
|
||||||
|
protected function parseFeature($path)
|
||||||
|
{
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
|
||||||
|
return $this->parser->parse($content, $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Loader;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loader interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface LoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if current loader supports provided resource.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function supports($resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads features from provided resource.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return list<FeatureNode>
|
||||||
|
*/
|
||||||
|
public function load($resource);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Loader;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yaml files loader.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class YamlFileLoader extends AbstractFileLoader
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LoaderInterface $loader = new ArrayLoader(),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if current loader supports provided resource.
|
||||||
|
*
|
||||||
|
* @param mixed $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function supports($resource)
|
||||||
|
{
|
||||||
|
return is_string($resource)
|
||||||
|
&& ($path = $this->findAbsolutePath($resource)) !== false
|
||||||
|
&& is_file($path)
|
||||||
|
&& pathinfo($path, PATHINFO_EXTENSION) === 'yml';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads features from provided resource.
|
||||||
|
*
|
||||||
|
* @param string $resource Resource to load
|
||||||
|
*
|
||||||
|
* @return FeatureNode[]
|
||||||
|
*/
|
||||||
|
public function load($resource)
|
||||||
|
{
|
||||||
|
$path = $this->getAbsolutePath($resource);
|
||||||
|
$hash = Yaml::parseFile($path);
|
||||||
|
|
||||||
|
$features = $this->loader->load($hash);
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
static fn (FeatureNode $feature) => new FeatureNode(
|
||||||
|
$feature->getTitle(),
|
||||||
|
$feature->getDescription(),
|
||||||
|
$feature->getTags(),
|
||||||
|
$feature->getBackground(),
|
||||||
|
$feature->getScenarios(),
|
||||||
|
$feature->getKeyword(),
|
||||||
|
$feature->getLanguage(),
|
||||||
|
$path,
|
||||||
|
$feature->getLine()
|
||||||
|
),
|
||||||
|
$features
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin arguments interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface ArgumentInterface extends NodeInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin Background.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class BackgroundNode implements ScenarioLikeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param StepNode[] $steps
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly ?string $title,
|
||||||
|
private readonly array $steps,
|
||||||
|
private readonly string $keyword,
|
||||||
|
private readonly int $line,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'Background';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns background title.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if background has steps.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasSteps()
|
||||||
|
{
|
||||||
|
return (bool) count($this->steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns background steps.
|
||||||
|
*
|
||||||
|
* @return StepNode[]
|
||||||
|
*/
|
||||||
|
public function getSteps()
|
||||||
|
{
|
||||||
|
return $this->steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns background keyword.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeyword()
|
||||||
|
{
|
||||||
|
return $this->keyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns background declaration line number.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine()
|
||||||
|
{
|
||||||
|
return $this->line;
|
||||||
|
}
|
||||||
|
}
|
||||||
+243
@@ -0,0 +1,243 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin Outline Example.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class ExampleNode implements ScenarioInterface, NamedScenarioInterface
|
||||||
|
{
|
||||||
|
use TaggedNodeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<StepNode>|null
|
||||||
|
*/
|
||||||
|
private ?array $steps = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $text The entire row as a string, e.g. "| 1 | 2 | 3 |"
|
||||||
|
* @param list<string> $tags
|
||||||
|
* @param array<array-key, StepNode> $outlineSteps
|
||||||
|
* @param array<string, string> $tokens
|
||||||
|
* @param int $line line number within the feature file
|
||||||
|
* @param string|null $outlineTitle original title of the scenario outline
|
||||||
|
* @param int|null $index the 1-based index of the row/example within the scenario outline
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $text,
|
||||||
|
private readonly array $tags,
|
||||||
|
private readonly array $outlineSteps,
|
||||||
|
private readonly array $tokens,
|
||||||
|
private readonly int $line,
|
||||||
|
private readonly ?string $outlineTitle = null,
|
||||||
|
private readonly ?int $index = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'Example';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node keyword.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeyword()
|
||||||
|
{
|
||||||
|
return $this->getNodeType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the example row as a single string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @deprecated you should normally not depend on the original row text, but if you really do, please switch
|
||||||
|
* to {@see self::getExampleText()} as this method will be removed in the next major version
|
||||||
|
*/
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return $this->text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTags()
|
||||||
|
{
|
||||||
|
return $this->tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if outline has steps.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasSteps()
|
||||||
|
{
|
||||||
|
return count($this->outlineSteps) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns outline steps.
|
||||||
|
*
|
||||||
|
* @return list<StepNode>
|
||||||
|
*/
|
||||||
|
public function getSteps()
|
||||||
|
{
|
||||||
|
return $this->steps ??= $this->createExampleSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns example tokens.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getTokens()
|
||||||
|
{
|
||||||
|
return $this->tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns outline declaration line number.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine()
|
||||||
|
{
|
||||||
|
return $this->line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns outline title.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getOutlineTitle()
|
||||||
|
{
|
||||||
|
return $this->outlineTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return "{$this->replaceTextTokens($this->outlineTitle)} #{$this->index}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the example row as a single string.
|
||||||
|
*
|
||||||
|
* You should normally not need this, since it is an implementation detail.
|
||||||
|
* If you need the individual example values, use {@see self::getTokens()}.
|
||||||
|
* To get the fully-normalised/expanded title, use {@see self::getName()}.
|
||||||
|
*/
|
||||||
|
public function getExampleText(): string
|
||||||
|
{
|
||||||
|
return $this->text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates steps for this example from abstract outline steps.
|
||||||
|
*
|
||||||
|
* @return list<StepNode>
|
||||||
|
*/
|
||||||
|
protected function createExampleSteps()
|
||||||
|
{
|
||||||
|
$steps = [];
|
||||||
|
foreach ($this->outlineSteps as $outlineStep) {
|
||||||
|
$keyword = $outlineStep->getKeyword();
|
||||||
|
$keywordType = $outlineStep->getKeywordType();
|
||||||
|
$text = $this->replaceTextTokens($outlineStep->getText());
|
||||||
|
$args = $this->replaceArgumentsTokens($outlineStep->getArguments());
|
||||||
|
$line = $outlineStep->getLine();
|
||||||
|
|
||||||
|
$steps[] = new StepNode($keyword, $text, $args, $line, $keywordType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces tokens in arguments with row values.
|
||||||
|
*
|
||||||
|
* @param array<array-key, ArgumentInterface> $arguments
|
||||||
|
*
|
||||||
|
* @return array<array-key, ArgumentInterface>
|
||||||
|
*/
|
||||||
|
protected function replaceArgumentsTokens(array $arguments)
|
||||||
|
{
|
||||||
|
foreach ($arguments as $num => $argument) {
|
||||||
|
if ($argument instanceof TableNode) {
|
||||||
|
$arguments[$num] = $this->replaceTableArgumentTokens($argument);
|
||||||
|
}
|
||||||
|
if ($argument instanceof PyStringNode) {
|
||||||
|
$arguments[$num] = $this->replacePyStringArgumentTokens($argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces tokens in table with row values.
|
||||||
|
*
|
||||||
|
* @return TableNode
|
||||||
|
*/
|
||||||
|
protected function replaceTableArgumentTokens(TableNode $argument)
|
||||||
|
{
|
||||||
|
$replacedTable = [];
|
||||||
|
foreach ($argument->getTable() as $line => $row) {
|
||||||
|
$replacedRow = [];
|
||||||
|
foreach ($row as $value) {
|
||||||
|
$replacedRow[] = $this->replaceTextTokens($value);
|
||||||
|
}
|
||||||
|
$replacedTable[$line] = $replacedRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TableNode($replacedTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces tokens in PyString with row values.
|
||||||
|
*
|
||||||
|
* @return PyStringNode
|
||||||
|
*/
|
||||||
|
protected function replacePyStringArgumentTokens(PyStringNode $argument)
|
||||||
|
{
|
||||||
|
$strings = $argument->getStrings();
|
||||||
|
foreach ($strings as $line => $string) {
|
||||||
|
$strings[$line] = $this->replaceTextTokens($strings[$line]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PyStringNode($strings, $argument->getLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces tokens in text with row values.
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function replaceTextTokens($text)
|
||||||
|
{
|
||||||
|
foreach ($this->tokens as $key => $val) {
|
||||||
|
$text = str_replace('<' . $key . '>', $val, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin Outline Example Table.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class ExampleTableNode extends TableNode implements TaggedNodeInterface
|
||||||
|
{
|
||||||
|
use TaggedNodeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, list<string>> $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
|
||||||
|
* @param list<string> $tags
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
array $table,
|
||||||
|
private readonly string $keyword,
|
||||||
|
private readonly array $tags = [],
|
||||||
|
) {
|
||||||
|
parent::__construct($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'ExampleTable';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTags()
|
||||||
|
{
|
||||||
|
return $this->tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns example table keyword.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeyword()
|
||||||
|
{
|
||||||
|
return $this->keyword;
|
||||||
|
}
|
||||||
|
}
|
||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
use function strlen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin Feature.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class FeatureNode implements KeywordNodeInterface, TaggedNodeInterface
|
||||||
|
{
|
||||||
|
use TaggedNodeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $tags
|
||||||
|
* @param ScenarioInterface[] $scenarios
|
||||||
|
* @param string|null $file the absolute path to the feature file
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly ?string $title,
|
||||||
|
private readonly ?string $description,
|
||||||
|
private readonly array $tags,
|
||||||
|
private readonly ?BackgroundNode $background,
|
||||||
|
private readonly array $scenarios,
|
||||||
|
private readonly string $keyword,
|
||||||
|
private readonly string $language,
|
||||||
|
private readonly ?string $file,
|
||||||
|
private readonly int $line,
|
||||||
|
) {
|
||||||
|
// Verify that the feature file is an absolute path.
|
||||||
|
if (!empty($file) && !$this->isAbsolutePath($file)) {
|
||||||
|
throw new InvalidArgumentException('The file should be an absolute path.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'Feature';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature title.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if feature has a description.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasDescription()
|
||||||
|
{
|
||||||
|
return !empty($this->description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature description.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getDescription()
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTags()
|
||||||
|
{
|
||||||
|
return $this->tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if feature has background.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasBackground()
|
||||||
|
{
|
||||||
|
return $this->background !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature background.
|
||||||
|
*
|
||||||
|
* @return BackgroundNode|null
|
||||||
|
*/
|
||||||
|
public function getBackground()
|
||||||
|
{
|
||||||
|
return $this->background;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if feature has scenarios.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasScenarios()
|
||||||
|
{
|
||||||
|
return count($this->scenarios) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature scenarios.
|
||||||
|
*
|
||||||
|
* @return ScenarioInterface[]
|
||||||
|
*/
|
||||||
|
public function getScenarios()
|
||||||
|
{
|
||||||
|
return $this->scenarios;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature keyword.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeyword()
|
||||||
|
{
|
||||||
|
return $this->keyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature language.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getLanguage()
|
||||||
|
{
|
||||||
|
return $this->language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature file as an absolute path.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getFile()
|
||||||
|
{
|
||||||
|
return $this->file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature declaration line number.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine()
|
||||||
|
{
|
||||||
|
return $this->line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of this feature, but with a different set of scenarios.
|
||||||
|
*
|
||||||
|
* @param array<array-key, ScenarioInterface> $scenarios
|
||||||
|
*/
|
||||||
|
public function withScenarios(array $scenarios): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$this->title,
|
||||||
|
$this->description,
|
||||||
|
$this->tags,
|
||||||
|
$this->background,
|
||||||
|
array_values($scenarios),
|
||||||
|
$this->keyword,
|
||||||
|
$this->language,
|
||||||
|
$this->file,
|
||||||
|
$this->line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the file path is an absolute path.
|
||||||
|
*
|
||||||
|
* @param string|null $file A file path
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @see https://github.com/symfony/filesystem/blob/master/Filesystem.php
|
||||||
|
*/
|
||||||
|
protected function isAbsolutePath($file)
|
||||||
|
{
|
||||||
|
if ($file === null) {
|
||||||
|
throw new InvalidArgumentException('The provided file path must not be null.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return strspn($file, '/\\', 0, 1)
|
||||||
|
|| (strlen($file) > 3 && ctype_alpha($file[0])
|
||||||
|
&& $file[1] === ':'
|
||||||
|
&& strspn($file, '/\\', 2, 1)
|
||||||
|
)
|
||||||
|
|| parse_url($file, PHP_URL_SCHEME) !== null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin keyword node interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface KeywordNodeInterface extends NodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns node keyword.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeyword();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node title.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getTitle();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
interface NamedScenarioInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the human-readable name of the scenario.
|
||||||
|
*/
|
||||||
|
public function getName(): ?string;
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin node interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface NodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns node type string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feature declaration line number.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine();
|
||||||
|
}
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin Outline.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class OutlineNode implements ScenarioInterface
|
||||||
|
{
|
||||||
|
use TaggedNodeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<array-key, ExampleTableNode>
|
||||||
|
*/
|
||||||
|
private array $tables;
|
||||||
|
/**
|
||||||
|
* @var ExampleNode[]
|
||||||
|
*/
|
||||||
|
private array $examples;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $tags
|
||||||
|
* @param StepNode[] $steps
|
||||||
|
* @param ExampleTableNode|array<array-key, ExampleTableNode> $tables
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly ?string $title,
|
||||||
|
private readonly array $tags,
|
||||||
|
private readonly array $steps,
|
||||||
|
array|ExampleTableNode $tables,
|
||||||
|
private readonly string $keyword,
|
||||||
|
private readonly int $line,
|
||||||
|
) {
|
||||||
|
$this->tables = is_array($tables) ? $tables : [$tables];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'Outline';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns outline title.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTags()
|
||||||
|
{
|
||||||
|
return $this->tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if outline has steps.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasSteps()
|
||||||
|
{
|
||||||
|
return count($this->steps) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns outline steps.
|
||||||
|
*
|
||||||
|
* @return StepNode[]
|
||||||
|
*/
|
||||||
|
public function getSteps()
|
||||||
|
{
|
||||||
|
return $this->steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if outline has examples.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasExamples()
|
||||||
|
{
|
||||||
|
return count($this->tables) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns examples table for the outline.
|
||||||
|
*
|
||||||
|
* WARNING: it returns a merged table with tags lost.
|
||||||
|
*
|
||||||
|
* @return ExampleTableNode
|
||||||
|
*
|
||||||
|
* @deprecated use getExampleTables instead
|
||||||
|
*/
|
||||||
|
public function getExampleTable()
|
||||||
|
{
|
||||||
|
$table = [];
|
||||||
|
foreach ($this->tables[0]->getTable() as $k => $v) {
|
||||||
|
$table[$k] = $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ExampleTableNode $exampleTableNode */
|
||||||
|
$exampleTableNode = new ExampleTableNode($table, $this->tables[0]->getKeyword());
|
||||||
|
$tableCount = count($this->tables);
|
||||||
|
for ($i = 1; $i < $tableCount; ++$i) {
|
||||||
|
$exampleTableNode->mergeRowsFromTable($this->tables[$i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $exampleTableNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of examples for the outline.
|
||||||
|
*
|
||||||
|
* @return ExampleNode[]
|
||||||
|
*/
|
||||||
|
public function getExamples()
|
||||||
|
{
|
||||||
|
return $this->examples ??= $this->createExamples();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns examples tables array for the outline.
|
||||||
|
*
|
||||||
|
* @return array<array-key, ExampleTableNode>
|
||||||
|
*/
|
||||||
|
public function getExampleTables()
|
||||||
|
{
|
||||||
|
return $this->tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns outline keyword.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeyword()
|
||||||
|
{
|
||||||
|
return $this->keyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns outline declaration line number.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine()
|
||||||
|
{
|
||||||
|
return $this->line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of this outline, but with a different table.
|
||||||
|
*
|
||||||
|
* @param array<array-key, ExampleTableNode> $exampleTables
|
||||||
|
*/
|
||||||
|
public function withTables(array $exampleTables): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$this->title,
|
||||||
|
$this->tags,
|
||||||
|
$this->steps,
|
||||||
|
$exampleTables,
|
||||||
|
$this->keyword,
|
||||||
|
$this->line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates examples for this outline using examples table.
|
||||||
|
*
|
||||||
|
* @return ExampleNode[]
|
||||||
|
*/
|
||||||
|
protected function createExamples()
|
||||||
|
{
|
||||||
|
$examples = [];
|
||||||
|
|
||||||
|
foreach ($this->getExampleTables() as $exampleTable) {
|
||||||
|
foreach ($exampleTable->getColumnsHash() as $rowNum => $row) {
|
||||||
|
$examples[] = new ExampleNode(
|
||||||
|
$exampleTable->getRowAsString($rowNum + 1),
|
||||||
|
array_merge($this->tags, $exampleTable->getTags()),
|
||||||
|
$this->getSteps(),
|
||||||
|
$row,
|
||||||
|
$exampleTable->getRowLine($rowNum + 1),
|
||||||
|
$this->getTitle(),
|
||||||
|
$rowNum + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $examples;
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin PyString argument.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class PyStringNode implements ArgumentInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param list<string> $strings String in form of [$stringLine]
|
||||||
|
* @param int $line Line number where string been started
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $strings,
|
||||||
|
private readonly int $line,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'PyString';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns entire PyString lines set.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getStrings()
|
||||||
|
{
|
||||||
|
return $this->strings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns raw string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRaw()
|
||||||
|
{
|
||||||
|
return implode("\n", $this->strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts PyString into string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this->getRaw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns line number at which PyString was started.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine()
|
||||||
|
{
|
||||||
|
return $this->line;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin scenario interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface ScenarioInterface extends ScenarioLikeInterface, TaggedNodeInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin scenario-like interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface ScenarioLikeInterface extends KeywordNodeInterface, StepContainerInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin Scenario.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class ScenarioNode implements ScenarioInterface, NamedScenarioInterface
|
||||||
|
{
|
||||||
|
use TaggedNodeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param StepNode[] $steps
|
||||||
|
* @param list<string> $tags
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly ?string $title,
|
||||||
|
private readonly array $tags,
|
||||||
|
private readonly array $steps,
|
||||||
|
private readonly string $keyword,
|
||||||
|
private readonly int $line,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'Scenario';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns scenario title.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*
|
||||||
|
* @deprecated you should use {@see self::getName()} instead as this method will be removed in the next
|
||||||
|
* major version
|
||||||
|
*/
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTags()
|
||||||
|
{
|
||||||
|
return $this->tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if scenario has steps.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasSteps()
|
||||||
|
{
|
||||||
|
return count($this->steps) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns scenario steps.
|
||||||
|
*
|
||||||
|
* @return StepNode[]
|
||||||
|
*/
|
||||||
|
public function getSteps()
|
||||||
|
{
|
||||||
|
return $this->steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns scenario keyword.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeyword()
|
||||||
|
{
|
||||||
|
return $this->keyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns scenario declaration line number.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine()
|
||||||
|
{
|
||||||
|
return $this->line;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin step container interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface StepContainerInterface extends NodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if container has steps.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasSteps();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns container steps.
|
||||||
|
*
|
||||||
|
* @return list<StepNode>
|
||||||
|
*/
|
||||||
|
public function getSteps();
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Exception\NodeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin Step.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
class StepNode implements NodeInterface
|
||||||
|
{
|
||||||
|
private readonly string $keywordType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ArgumentInterface[] $arguments
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $keyword,
|
||||||
|
private readonly string $text,
|
||||||
|
private readonly array $arguments,
|
||||||
|
private readonly int $line,
|
||||||
|
?string $keywordType = null,
|
||||||
|
) {
|
||||||
|
if (count($arguments) > 1) {
|
||||||
|
throw new NodeException(sprintf(
|
||||||
|
'Steps could have only one argument, but `%s %s` have %d.',
|
||||||
|
$keyword,
|
||||||
|
$text,
|
||||||
|
count($arguments)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->keywordType = $keywordType ?: 'Given';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'Step';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns step keyword in provided language (Given, When, Then, etc.).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @deprecated use getKeyword() instead
|
||||||
|
*/
|
||||||
|
public function getType()
|
||||||
|
{
|
||||||
|
return $this->getKeyword();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns step keyword in provided language (Given, When, Then, etc.).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeyword()
|
||||||
|
{
|
||||||
|
return $this->keyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns step type keyword (Given, When, Then, etc.).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKeywordType()
|
||||||
|
{
|
||||||
|
return $this->keywordType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns step text.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getText()
|
||||||
|
{
|
||||||
|
return $this->text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if step has arguments.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasArguments()
|
||||||
|
{
|
||||||
|
return (bool) count($this->arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns step arguments.
|
||||||
|
*
|
||||||
|
* @return ArgumentInterface[]
|
||||||
|
*/
|
||||||
|
public function getArguments()
|
||||||
|
{
|
||||||
|
return $this->arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns step declaration line number.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine()
|
||||||
|
{
|
||||||
|
return $this->line;
|
||||||
|
}
|
||||||
|
}
|
||||||
+382
@@ -0,0 +1,382 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
use ArrayIterator;
|
||||||
|
use Behat\Gherkin\Exception\NodeException;
|
||||||
|
use Iterator;
|
||||||
|
use IteratorAggregate;
|
||||||
|
use ReturnTypeWillChange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Gherkin Table argument.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* @template-implements IteratorAggregate<int, array<string, string>>
|
||||||
|
*/
|
||||||
|
class TableNode implements ArgumentInterface, IteratorAggregate
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<array-key, int>
|
||||||
|
*/
|
||||||
|
private array $maxLineLength = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes table.
|
||||||
|
*
|
||||||
|
* @param array<int, list<string>> $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
|
||||||
|
*
|
||||||
|
* @throws NodeException If the given table is invalid
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private array $table,
|
||||||
|
) {
|
||||||
|
$columnCount = null;
|
||||||
|
|
||||||
|
foreach ($this->getRows() as $ridx => $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
throw new NodeException(sprintf(
|
||||||
|
"Table row '%s' is expected to be array, got %s",
|
||||||
|
$ridx,
|
||||||
|
gettype($row)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($columnCount === null) {
|
||||||
|
$columnCount = count($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($row) !== $columnCount) {
|
||||||
|
throw new NodeException(sprintf(
|
||||||
|
"Table row '%s' is expected to have %s columns, got %s",
|
||||||
|
$ridx,
|
||||||
|
$columnCount,
|
||||||
|
count($row)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($row as $column => $string) {
|
||||||
|
if (!isset($this->maxLineLength[$column])) {
|
||||||
|
$this->maxLineLength[$column] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_scalar($string)) {
|
||||||
|
throw new NodeException(sprintf(
|
||||||
|
"Table cell at row '%s', col '%s' is expected to be scalar, got %s",
|
||||||
|
$ridx,
|
||||||
|
$column,
|
||||||
|
gettype($string)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->maxLineLength[$column] = max($this->maxLineLength[$column], mb_strlen($string, 'utf8'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a table from a given list.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $list One-dimensional array
|
||||||
|
*
|
||||||
|
* @return TableNode
|
||||||
|
*
|
||||||
|
* @throws NodeException If the given list is not a one-dimensional array
|
||||||
|
*/
|
||||||
|
public static function fromList(array $list)
|
||||||
|
{
|
||||||
|
if (count($list) !== count($list, COUNT_RECURSIVE)) {
|
||||||
|
throw new NodeException('List is not a one-dimensional array.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = array_map(fn ($item) => [$item], $list);
|
||||||
|
|
||||||
|
return new self($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node type.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getNodeType()
|
||||||
|
{
|
||||||
|
return 'Table';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns table hash, formed by columns (ColumnsHash).
|
||||||
|
*
|
||||||
|
* @return list<array<string, string>>
|
||||||
|
*/
|
||||||
|
public function getHash()
|
||||||
|
{
|
||||||
|
return $this->getColumnsHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns table hash, formed by columns.
|
||||||
|
*
|
||||||
|
* @return list<array<string, string>>
|
||||||
|
*/
|
||||||
|
public function getColumnsHash()
|
||||||
|
{
|
||||||
|
$rows = $this->getRows();
|
||||||
|
$keys = array_shift($rows);
|
||||||
|
|
||||||
|
$hash = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
\assert($keys !== null); // If there is no first row due to an empty table, we won't enter this loop either.
|
||||||
|
$hash[] = array_combine($keys, $row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns table hash, formed by rows.
|
||||||
|
*
|
||||||
|
* @return array<string, string|list<string>>
|
||||||
|
*/
|
||||||
|
public function getRowsHash()
|
||||||
|
{
|
||||||
|
$hash = [];
|
||||||
|
|
||||||
|
foreach ($this->getRows() as $row) {
|
||||||
|
$hash[array_shift($row)] = count($row) === 1 ? $row[0] : $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns numerated table lines.
|
||||||
|
* Line numbers are keys, lines are values.
|
||||||
|
*
|
||||||
|
* @return array<int, list<string>>
|
||||||
|
*/
|
||||||
|
public function getTable()
|
||||||
|
{
|
||||||
|
return $this->table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns table rows.
|
||||||
|
*
|
||||||
|
* @return list<list<string>>
|
||||||
|
*/
|
||||||
|
public function getRows()
|
||||||
|
{
|
||||||
|
return array_values($this->table);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns table definition lines.
|
||||||
|
*
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
public function getLines()
|
||||||
|
{
|
||||||
|
return array_keys($this->table);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns specific row in a table.
|
||||||
|
*
|
||||||
|
* @param int $index Row number
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*
|
||||||
|
* @throws NodeException If row with specified index does not exist
|
||||||
|
*/
|
||||||
|
public function getRow($index)
|
||||||
|
{
|
||||||
|
$rows = $this->getRows();
|
||||||
|
|
||||||
|
if (!isset($rows[$index])) {
|
||||||
|
throw new NodeException(sprintf('Rows #%d does not exist in table.', $index));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows[$index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns specific column in a table.
|
||||||
|
*
|
||||||
|
* @param int $index Column number
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*
|
||||||
|
* @throws NodeException If column with specified index does not exist
|
||||||
|
*/
|
||||||
|
public function getColumn($index)
|
||||||
|
{
|
||||||
|
if ($index >= count($this->getRow(0))) {
|
||||||
|
throw new NodeException(sprintf('Column #%d does not exist in table.', $index));
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $this->getRows();
|
||||||
|
$column = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$column[] = $row[$index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns line number at which specific row was defined.
|
||||||
|
*
|
||||||
|
* @param int $index
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*
|
||||||
|
* @throws NodeException If row with specified index does not exist
|
||||||
|
*/
|
||||||
|
public function getRowLine($index)
|
||||||
|
{
|
||||||
|
$lines = array_keys($this->table);
|
||||||
|
|
||||||
|
if (!isset($lines[$index])) {
|
||||||
|
throw new NodeException(sprintf('Rows #%d does not exist in table.', $index));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lines[$index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts row into delimited string.
|
||||||
|
*
|
||||||
|
* @param int $rowNum Row number
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRowAsString($rowNum)
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
foreach ($this->getRow($rowNum) as $column => $value) {
|
||||||
|
$values[] = $this->padRight(' ' . $value . ' ', $this->maxLineLength[$column] + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('|%s|', implode('|', $values));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts row into delimited string.
|
||||||
|
*
|
||||||
|
* @param int $rowNum Row number
|
||||||
|
* @param callable(string, int): string $wrapper Wrapper function
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRowAsStringWithWrappedValues($rowNum, $wrapper)
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
foreach ($this->getRow($rowNum) as $column => $value) {
|
||||||
|
$value = $this->padRight(' ' . $value . ' ', $this->maxLineLength[$column] + 2);
|
||||||
|
|
||||||
|
$values[] = call_user_func($wrapper, $value, $column);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('|%s|', implode('|', $values));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts entire table into string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTableAsString()
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
$rowCount = count($this->getRows());
|
||||||
|
for ($i = 0; $i < $rowCount; ++$i) {
|
||||||
|
$lines[] = $this->getRowAsString($i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns line number at which table was started.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getLine()
|
||||||
|
{
|
||||||
|
return $this->getRowLine(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts table into string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this->getTableAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a hash iterator.
|
||||||
|
*
|
||||||
|
* @return Iterator
|
||||||
|
*/
|
||||||
|
#[ReturnTypeWillChange]
|
||||||
|
public function getIterator()
|
||||||
|
{
|
||||||
|
return new ArrayIterator($this->getHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains and adds rows from another table to the current table.
|
||||||
|
* The second table should have the same structure as the current one.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @deprecated remove together with OutlineNode::getExampleTable
|
||||||
|
*/
|
||||||
|
public function mergeRowsFromTable(TableNode $node)
|
||||||
|
{
|
||||||
|
// check structure
|
||||||
|
if ($this->getRow(0) !== $node->getRow(0)) {
|
||||||
|
throw new NodeException('Tables have different structure. Cannot merge one into another');
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstLine = $node->getLine();
|
||||||
|
foreach ($node->getTable() as $line => $value) {
|
||||||
|
if ($line === $firstLine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->table[$line] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pads string right.
|
||||||
|
*
|
||||||
|
* @param string $text Text to pad
|
||||||
|
* @param int $length Length
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function padRight($text, $length)
|
||||||
|
{
|
||||||
|
while ($length > mb_strlen($text, 'utf8')) {
|
||||||
|
$text .= ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin tagged node interface.
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*/
|
||||||
|
interface TaggedNodeInterface extends NodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if node is tagged with tag.
|
||||||
|
*
|
||||||
|
* @param string $tag
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasTag($tag);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if node has tags (including any inherited tags e.g. from feature).
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasTags();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns node tags (including any inherited tags e.g. from feature).
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getTags();
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This trait partially implements {@see TaggedNodeInterface}.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
trait TaggedNodeTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
abstract public function getTags();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $tag
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasTag($tag)
|
||||||
|
{
|
||||||
|
return in_array($tag, $this->getTags(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasTags()
|
||||||
|
{
|
||||||
|
return $this->getTags() !== [];
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+690
@@ -0,0 +1,690 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Behat Gherkin Parser.
|
||||||
|
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Behat\Gherkin;
|
||||||
|
|
||||||
|
use Behat\Gherkin\Exception\LexerException;
|
||||||
|
use Behat\Gherkin\Exception\NodeException;
|
||||||
|
use Behat\Gherkin\Exception\ParserException;
|
||||||
|
use Behat\Gherkin\Exception\UnexpectedParserNodeException;
|
||||||
|
use Behat\Gherkin\Exception\UnexpectedTaggedNodeException;
|
||||||
|
use Behat\Gherkin\Node\BackgroundNode;
|
||||||
|
use Behat\Gherkin\Node\ExampleTableNode;
|
||||||
|
use Behat\Gherkin\Node\FeatureNode;
|
||||||
|
use Behat\Gherkin\Node\OutlineNode;
|
||||||
|
use Behat\Gherkin\Node\PyStringNode;
|
||||||
|
use Behat\Gherkin\Node\ScenarioInterface;
|
||||||
|
use Behat\Gherkin\Node\ScenarioNode;
|
||||||
|
use Behat\Gherkin\Node\StepNode;
|
||||||
|
use Behat\Gherkin\Node\TableNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gherkin parser.
|
||||||
|
*
|
||||||
|
* $lexer = new Behat\Gherkin\Lexer($keywords);
|
||||||
|
* $parser = new Behat\Gherkin\Parser($lexer);
|
||||||
|
* $featuresArray = $parser->parse('/path/to/feature.feature');
|
||||||
|
*
|
||||||
|
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||||
|
*
|
||||||
|
* @phpstan-import-type TToken from Lexer
|
||||||
|
*
|
||||||
|
* @phpstan-type TParsedExpressionResult FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|ExampleTableNode|TableNode|PyStringNode|StepNode|string
|
||||||
|
*/
|
||||||
|
class Parser
|
||||||
|
{
|
||||||
|
private string $input;
|
||||||
|
private ?string $file = null;
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private array $tags = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly Lexer $lexer,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses input & returns features array.
|
||||||
|
*
|
||||||
|
* @param string $input Gherkin string document
|
||||||
|
* @param string|null $file File name
|
||||||
|
*
|
||||||
|
* @return FeatureNode|null
|
||||||
|
*
|
||||||
|
* @throws ParserException
|
||||||
|
*/
|
||||||
|
public function parse($input, $file = null)
|
||||||
|
{
|
||||||
|
$this->input = $input;
|
||||||
|
$this->file = $file;
|
||||||
|
$this->tags = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->lexer->analyse($this->input, 'en');
|
||||||
|
} catch (LexerException $e) {
|
||||||
|
throw new ParserException(
|
||||||
|
sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$feature = null;
|
||||||
|
while ($this->predictTokenType() !== 'EOS') {
|
||||||
|
$node = $this->parseExpression();
|
||||||
|
|
||||||
|
if ($node === "\n" || $node === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$feature && $node instanceof FeatureNode) {
|
||||||
|
$feature = $node;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnexpectedParserNodeException('Feature', $node, $this->file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns next token if it's type equals to expected.
|
||||||
|
*
|
||||||
|
* @param string $type Token type
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken
|
||||||
|
*
|
||||||
|
* @throws ParserException
|
||||||
|
*/
|
||||||
|
protected function expectTokenType($type)
|
||||||
|
{
|
||||||
|
$types = (array) $type;
|
||||||
|
if (in_array($this->predictTokenType(), $types)) {
|
||||||
|
return $this->lexer->getAdvancedToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->lexer->predictToken();
|
||||||
|
|
||||||
|
throw new ParserException(sprintf(
|
||||||
|
'Expected %s token, but got %s on line: %d%s',
|
||||||
|
implode(' or ', $types),
|
||||||
|
$this->predictTokenType(),
|
||||||
|
$token['line'],
|
||||||
|
$this->file ? ' in file: ' . $this->file : ''
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns next token if it's type equals to expected.
|
||||||
|
*
|
||||||
|
* @param string $type Token type
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*
|
||||||
|
* @phpstan-return TToken|null
|
||||||
|
*/
|
||||||
|
protected function acceptTokenType($type)
|
||||||
|
{
|
||||||
|
if ($type !== $this->predictTokenType()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->lexer->getAdvancedToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns next token type without real input reading (prediction).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function predictTokenType()
|
||||||
|
{
|
||||||
|
$token = $this->lexer->predictToken();
|
||||||
|
|
||||||
|
return $token['type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses current expression & returns Node.
|
||||||
|
*
|
||||||
|
* @phpstan-return TParsedExpressionResult
|
||||||
|
*
|
||||||
|
* @throws ParserException
|
||||||
|
*/
|
||||||
|
protected function parseExpression()
|
||||||
|
{
|
||||||
|
$type = $this->predictTokenType();
|
||||||
|
|
||||||
|
while ($type === 'Comment') {
|
||||||
|
$this->expectTokenType('Comment');
|
||||||
|
|
||||||
|
$type = $this->predictTokenType();
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
'Feature' => $this->parseFeature(),
|
||||||
|
'Background' => $this->parseBackground(),
|
||||||
|
'Scenario' => $this->parseScenario(),
|
||||||
|
'Outline' => $this->parseOutline(),
|
||||||
|
'Examples' => $this->parseExamples(),
|
||||||
|
'TableRow' => $this->parseTable(),
|
||||||
|
'PyStringOp' => $this->parsePyString(),
|
||||||
|
'Step' => $this->parseStep(),
|
||||||
|
'Text' => $this->parseText(),
|
||||||
|
'Newline' => $this->parseNewline(),
|
||||||
|
'Tag' => $this->parseTags(),
|
||||||
|
'Language' => $this->parseLanguage(),
|
||||||
|
'EOS' => '',
|
||||||
|
default => throw new ParserException(sprintf('Unknown token type: %s', $type)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses feature token & returns it's node.
|
||||||
|
*
|
||||||
|
* @return FeatureNode
|
||||||
|
*
|
||||||
|
* @throws ParserException
|
||||||
|
*/
|
||||||
|
protected function parseFeature()
|
||||||
|
{
|
||||||
|
$token = $this->expectTokenType('Feature');
|
||||||
|
\assert(\array_key_exists('keyword', $token));
|
||||||
|
\assert(\array_key_exists('indent', $token));
|
||||||
|
|
||||||
|
$title = trim($token['value'] ?? '');
|
||||||
|
$description = null;
|
||||||
|
$tags = $this->popTags();
|
||||||
|
$background = null;
|
||||||
|
$scenarios = [];
|
||||||
|
$keyword = $token['keyword'];
|
||||||
|
$language = $this->lexer->getLanguage();
|
||||||
|
$file = $this->file;
|
||||||
|
$line = $token['line'];
|
||||||
|
|
||||||
|
// Parse description, background, scenarios & outlines
|
||||||
|
while ($this->predictTokenType() !== 'EOS') {
|
||||||
|
$node = $this->parseExpression();
|
||||||
|
|
||||||
|
if (is_string($node)) {
|
||||||
|
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
|
||||||
|
$description .= ($description !== null ? "\n" : '') . $text;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isBackgroundAllowed = ($background === null && $scenarios === []);
|
||||||
|
|
||||||
|
if ($isBackgroundAllowed && $node instanceof BackgroundNode) {
|
||||||
|
$background = $node;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node instanceof ScenarioInterface) {
|
||||||
|
$scenarios[] = $node;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnexpectedParserNodeException(
|
||||||
|
match ($isBackgroundAllowed) {
|
||||||
|
true => 'Background, Scenario or Outline',
|
||||||
|
false => 'Scenario or Outline',
|
||||||
|
},
|
||||||
|
$node,
|
||||||
|
$this->file,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FeatureNode(
|
||||||
|
rtrim($title) ?: null,
|
||||||
|
rtrim($description ?? '') ?: null,
|
||||||
|
$tags,
|
||||||
|
$background,
|
||||||
|
$scenarios,
|
||||||
|
$keyword,
|
||||||
|
$language,
|
||||||
|
$file,
|
||||||
|
$line
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses background token & returns it's node.
|
||||||
|
*
|
||||||
|
* @return BackgroundNode
|
||||||
|
*
|
||||||
|
* @throws ParserException
|
||||||
|
*/
|
||||||
|
protected function parseBackground()
|
||||||
|
{
|
||||||
|
$token = $this->expectTokenType('Background');
|
||||||
|
\assert(\array_key_exists('keyword', $token));
|
||||||
|
\assert(\array_key_exists('indent', $token));
|
||||||
|
|
||||||
|
$title = trim($token['value'] ?? '');
|
||||||
|
$keyword = $token['keyword'];
|
||||||
|
$line = $token['line'];
|
||||||
|
|
||||||
|
if (count($this->popTags()) !== 0) {
|
||||||
|
// Should not be possible to happen, parseTags should have already picked this up.
|
||||||
|
throw new UnexpectedTaggedNodeException($token, $this->file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse description and steps
|
||||||
|
$steps = [];
|
||||||
|
$allowedTokenTypes = ['Step', 'Newline', 'Text', 'Comment'];
|
||||||
|
while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
|
||||||
|
$node = $this->parseExpression();
|
||||||
|
|
||||||
|
if ($node instanceof StepNode) {
|
||||||
|
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($steps === [] && is_string($node)) {
|
||||||
|
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
|
||||||
|
$title .= "\n" . $text;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node === "\n") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnexpectedParserNodeException('Step', $node, $this->file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses scenario token & returns it's node.
|
||||||
|
*
|
||||||
|
* @return OutlineNode|ScenarioNode
|
||||||
|
*
|
||||||
|
* @throws ParserException
|
||||||
|
*/
|
||||||
|
protected function parseScenario()
|
||||||
|
{
|
||||||
|
return $this->parseScenarioOrOutlineBody($this->expectTokenType('Scenario'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses scenario outline token & returns it's node.
|
||||||
|
*
|
||||||
|
* @return OutlineNode|ScenarioNode
|
||||||
|
*
|
||||||
|
* @throws ParserException
|
||||||
|
*/
|
||||||
|
protected function parseOutline()
|
||||||
|
{
|
||||||
|
return $this->parseScenarioOrOutlineBody($this->expectTokenType('Outline'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-param TToken $token
|
||||||
|
*/
|
||||||
|
private function parseScenarioOrOutlineBody(array $token): OutlineNode|ScenarioNode
|
||||||
|
{
|
||||||
|
\assert(\array_key_exists('keyword', $token));
|
||||||
|
\assert(\array_key_exists('indent', $token));
|
||||||
|
$title = trim($token['value'] ?? '');
|
||||||
|
$tags = $this->popTags();
|
||||||
|
$keyword = $token['keyword'];
|
||||||
|
|
||||||
|
/** @var list<ExampleTableNode> $examples */
|
||||||
|
$examples = [];
|
||||||
|
$line = $token['line'];
|
||||||
|
|
||||||
|
// Parse description, steps and examples
|
||||||
|
$steps = [];
|
||||||
|
|
||||||
|
while (in_array($nextTokenType = $this->predictTokenType(), ['Step', 'Examples', 'Newline', 'Text', 'Comment', 'Tag'])) {
|
||||||
|
if ($nextTokenType === 'Comment') {
|
||||||
|
$this->lexer->skipPredictedToken();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nextTokenType === 'Tag') {
|
||||||
|
// The only thing inside a Scenario / Scenario Outline that can be tagged is an Examples table
|
||||||
|
// Scan on to see what the tags are attached to - if it's not Examples then we must have reached the
|
||||||
|
// end of this scenario and be about to start a new one.
|
||||||
|
if ($this->validateAndGetNextTaggedNodeType() !== 'Examples') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = $this->parseExpression();
|
||||||
|
|
||||||
|
if ($steps === [] && is_string($node)) {
|
||||||
|
// Free text is only allowed before the first step (or when parsing Examples: which is done elsewhere)
|
||||||
|
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
|
||||||
|
$title .= "\n" . $text;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node === "\n") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($examples === [] && $node instanceof StepNode) {
|
||||||
|
// Steps are only allowed before the first Examples table (if any)
|
||||||
|
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node instanceof ExampleTableNode) {
|
||||||
|
// NB: It is valid to have a Scenario with Examples: but no Steps
|
||||||
|
// It is also valid to have an Examples: with no table rows (this produces no actual examples)
|
||||||
|
$examples[] = $node;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnexpectedParserNodeException(
|
||||||
|
match ($examples) {
|
||||||
|
[] => 'Step, Examples table, or end of Scenario',
|
||||||
|
default => 'Examples table or end of Scenario',
|
||||||
|
},
|
||||||
|
$node,
|
||||||
|
$this->file,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($examples !== []) {
|
||||||
|
return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peek ahead to find the node that the current tags belong to.
|
||||||
|
*
|
||||||
|
* @throws UnexpectedTaggedNodeException if there is not a taggable node
|
||||||
|
*/
|
||||||
|
private function validateAndGetNextTaggedNodeType(): string
|
||||||
|
{
|
||||||
|
$deferred = [];
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
$deferred[] = $next = $this->lexer->getAdvancedToken();
|
||||||
|
$nextType = $next['type'];
|
||||||
|
|
||||||
|
if (in_array($nextType, ['Tag', 'Comment', 'Newline'], true)) {
|
||||||
|
// These are the only node types allowed between tag node(s) and the node they are tagging
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($nextType, ['Feature', 'Examples', 'Scenario', 'Outline'], true)) {
|
||||||
|
// These are the only taggable node types
|
||||||
|
return $nextType;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnexpectedTaggedNodeException($next, $this->file);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Rewind the lexer back to where it was when we started scanning ahead
|
||||||
|
foreach ($deferred as $token) {
|
||||||
|
$this->lexer->deferToken($token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses step token & returns it's node.
|
||||||
|
*
|
||||||
|
* @return StepNode
|
||||||
|
*/
|
||||||
|
protected function parseStep()
|
||||||
|
{
|
||||||
|
$token = $this->expectTokenType('Step');
|
||||||
|
\assert(\is_string($token['value']));
|
||||||
|
\assert(\array_key_exists('keyword_type', $token));
|
||||||
|
\assert(\array_key_exists('text', $token));
|
||||||
|
|
||||||
|
$keyword = $token['value'];
|
||||||
|
$keywordType = $token['keyword_type'];
|
||||||
|
$text = trim($token['text']);
|
||||||
|
$line = $token['line'];
|
||||||
|
|
||||||
|
$arguments = [];
|
||||||
|
while (in_array($predicted = $this->predictTokenType(), ['PyStringOp', 'TableRow', 'Newline', 'Comment'])) {
|
||||||
|
if ($predicted === 'Comment' || $predicted === 'Newline') {
|
||||||
|
$this->acceptTokenType($predicted);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = $this->parseExpression();
|
||||||
|
|
||||||
|
if ($node instanceof PyStringNode || $node instanceof TableNode) {
|
||||||
|
$arguments[] = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepNode($keyword, $text, $arguments, $line, $keywordType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses examples table node.
|
||||||
|
*
|
||||||
|
* @return ExampleTableNode
|
||||||
|
*/
|
||||||
|
protected function parseExamples()
|
||||||
|
{
|
||||||
|
$token = $this->expectTokenType('Examples');
|
||||||
|
\assert(\array_key_exists('keyword', $token));
|
||||||
|
$keyword = $token['keyword'];
|
||||||
|
$tags = empty($this->tags) ? [] : $this->popTags();
|
||||||
|
$table = $this->parseTableRows();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new ExampleTableNode($table, $keyword, $tags);
|
||||||
|
} catch (NodeException $e) {
|
||||||
|
$this->rethrowNodeException($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses table token & returns it's node.
|
||||||
|
*
|
||||||
|
* @return TableNode
|
||||||
|
*/
|
||||||
|
protected function parseTable()
|
||||||
|
{
|
||||||
|
$table = $this->parseTableRows();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new TableNode($table);
|
||||||
|
} catch (NodeException $e) {
|
||||||
|
$this->rethrowNodeException($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses PyString token & returns it's node.
|
||||||
|
*
|
||||||
|
* @return PyStringNode
|
||||||
|
*/
|
||||||
|
protected function parsePyString()
|
||||||
|
{
|
||||||
|
$token = $this->expectTokenType('PyStringOp');
|
||||||
|
|
||||||
|
$line = $token['line'];
|
||||||
|
|
||||||
|
$strings = [];
|
||||||
|
while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && $predicted === 'Text') {
|
||||||
|
$token = $this->expectTokenType('Text');
|
||||||
|
\assert(\is_string($token['value']));
|
||||||
|
|
||||||
|
$strings[] = $token['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->expectTokenType('PyStringOp');
|
||||||
|
|
||||||
|
return new PyStringNode($strings, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses tags.
|
||||||
|
*
|
||||||
|
* @phpstan-return TParsedExpressionResult
|
||||||
|
*/
|
||||||
|
protected function parseTags()
|
||||||
|
{
|
||||||
|
$token = $this->expectTokenType('Tag');
|
||||||
|
\assert(\array_key_exists('tags', $token));
|
||||||
|
|
||||||
|
// Validate that the tags are followed by a node that can be tagged
|
||||||
|
$this->validateAndGetNextTaggedNodeType();
|
||||||
|
|
||||||
|
$this->guardTags($token['tags']);
|
||||||
|
|
||||||
|
$this->tags = array_merge($this->tags, $token['tags']);
|
||||||
|
|
||||||
|
return "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current set of tags and clears tag buffer.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
protected function popTags()
|
||||||
|
{
|
||||||
|
$tags = $this->tags;
|
||||||
|
$this->tags = [];
|
||||||
|
|
||||||
|
return $tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the tags fit the required format.
|
||||||
|
*
|
||||||
|
* @param string[] $tags
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function guardTags(array $tags)
|
||||||
|
{
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
if (preg_match('/\s/', $tag)) {
|
||||||
|
trigger_error(
|
||||||
|
sprintf('Whitespace in tags is deprecated, found "%s"', $tag),
|
||||||
|
E_USER_DEPRECATED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses next text line & returns it.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function parseText()
|
||||||
|
{
|
||||||
|
$token = $this->expectTokenType('Text');
|
||||||
|
\assert(\is_string($token['value']));
|
||||||
|
|
||||||
|
return $token['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses next newline & returns \n.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function parseNewline()
|
||||||
|
{
|
||||||
|
$this->expectTokenType('Newline');
|
||||||
|
|
||||||
|
return "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips over language tags (they are handled inside the Lexer).
|
||||||
|
*
|
||||||
|
* @phpstan-return TParsedExpressionResult
|
||||||
|
*
|
||||||
|
* @throws ParserException
|
||||||
|
*
|
||||||
|
* @deprecated language tags are handled inside the Lexer, they skipped over (like any other comment) in the Parser
|
||||||
|
*/
|
||||||
|
protected function parseLanguage()
|
||||||
|
{
|
||||||
|
$this->expectTokenType('Language');
|
||||||
|
|
||||||
|
return $this->parseExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the rows of a table.
|
||||||
|
*
|
||||||
|
* @return array<int, list<string>>
|
||||||
|
*/
|
||||||
|
private function parseTableRows()
|
||||||
|
{
|
||||||
|
$table = [];
|
||||||
|
while (in_array($predicted = $this->predictTokenType(), ['TableRow', 'Newline', 'Comment'])) {
|
||||||
|
if ($predicted === 'Comment' || $predicted === 'Newline') {
|
||||||
|
$this->acceptTokenType($predicted);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->expectTokenType('TableRow');
|
||||||
|
\assert(\array_key_exists('columns', $token));
|
||||||
|
|
||||||
|
$table[$token['line']] = $token['columns'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes step node type for types But, And to type of previous step if it exists else sets to Given.
|
||||||
|
*
|
||||||
|
* @param StepNode[] $steps
|
||||||
|
*
|
||||||
|
* @return StepNode
|
||||||
|
*/
|
||||||
|
private function normalizeStepNodeKeywordType(StepNode $node, array $steps = [])
|
||||||
|
{
|
||||||
|
if (!in_array($node->getKeywordType(), ['And', 'But'])) {
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prev = end($steps)) {
|
||||||
|
$keywordType = $prev->getKeywordType();
|
||||||
|
} else {
|
||||||
|
$keywordType = 'Given';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepNode(
|
||||||
|
$node->getKeyword(),
|
||||||
|
$node->getText(),
|
||||||
|
$node->getArguments(),
|
||||||
|
$node->getLine(),
|
||||||
|
$keywordType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function rethrowNodeException(NodeException $e): never
|
||||||
|
{
|
||||||
|
throw new ParserException(
|
||||||
|
$e->getMessage() . ($this->file ? ' in file ' . $this->file : ''),
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../codeception/codeception/codecept)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/codeception/codeception/codecept');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/codeception/codeception/codecept';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/codecept
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../cebe/markdown/bin/markdown)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/cebe/markdown/bin/markdown');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/cebe/markdown/bin/markdown';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/markdown
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/php-parse
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = 'phpvfscomposer://'.$this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
|
||||||
|
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/phpunit
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../psy/psysh/bin/psysh)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/psy/psysh/bin/psysh';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/psysh
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user