From 4469c4a7bbb58fe37baf7b21c110ac548e1c93ae Mon Sep 17 00:00:00 2001 From: Rafael White Date: Mon, 20 Apr 2020 12:41:30 -0400 Subject: [PATCH] Merge commit 'ccc61e17d9078b717fc8ce213097a9fca741dc73' as 'src/http' --- .env.example | 23 + .gitignore | 17 + LICENSE.md | 19 + README.md | 315 +++ codeception.yml | 21 + composer.json | 36 + phinx.php | 33 + phpcs.xml | 57 + src/Api/BaseController.php | 122 + src/Api/CrudCustomFieldsController.php | 205 ++ src/Contracts/Api/CrudBehaviorTrait.php | 280 ++ .../Api/CrudCustomFieldsBehaviorTrait.php | 130 + .../Api/CrudElasticBehaviorTrait.php | 67 + .../Converter/ConverterInterface.php | 13 + .../Converter/CustomQueriesTrait.php | 98 + src/Converter/RequestUriToElasticSearch.php | 505 ++++ src/Converter/RequestUriToSql.php | 987 +++++++ src/Middleware/Response.php | 35 + src/Request/Baka.php | 34 + src/Request/Swoole.php | 1345 ++++++++++ src/Response/Swoole.php | 86 + src/Router/Collection.php | 239 ++ storage/db/migrations/20190408003104_ls.php | 138 + storage/db/migrations/schema.php | 680 +++++ storage/db/seeds/LeadSeeder.php | 61 + tests/_bootstrap.php | 30 + tests/_data/dump.sql | 1 + tests/_output/.gitignore | 2 + tests/_support/AcceptanceTester.php | 26 + tests/_support/FunctionalTester.php | 26 + tests/_support/Helper/Acceptance.php | 10 + tests/_support/Helper/Functional.php | 10 + tests/_support/Helper/Unit.php | 10 + tests/_support/Indices/Leads.php | 70 + tests/_support/Model/Leads.php | 16 + tests/_support/UnitTester.php | 26 + .../_generated/AcceptanceTesterActions.php | 2382 +++++++++++++++++ .../_generated/FunctionalTesterActions.php | 16 + .../_support/_generated/UnitTesterActions.php | 609 +++++ tests/acceptance.suite.yml | 12 + tests/acceptance/_bootstrap.php | 2 + tests/functional.suite.yml | 11 + tests/functional/_bootstrap.php | 2 + tests/unit.suite.yml | 9 + tests/unit/PhalconUnitTestCase.php | 99 + tests/unit/UriToElasticSqlTest.php | 401 +++ tests/unit/UriToSqlTest.php | 352 +++ tests/unit/_bootstrap.php | 2 + 48 files changed, 9670 insertions(+) create mode 100644 .env.example create mode 100755 .gitignore create mode 100644 LICENSE.md create mode 100755 README.md create mode 100755 codeception.yml create mode 100755 composer.json create mode 100644 phinx.php create mode 100755 phpcs.xml create mode 100644 src/Api/BaseController.php create mode 100644 src/Api/CrudCustomFieldsController.php create mode 100644 src/Contracts/Api/CrudBehaviorTrait.php create mode 100644 src/Contracts/Api/CrudCustomFieldsBehaviorTrait.php create mode 100644 src/Contracts/Api/CrudElasticBehaviorTrait.php create mode 100644 src/Contracts/Converter/ConverterInterface.php create mode 100644 src/Contracts/Converter/CustomQueriesTrait.php create mode 100644 src/Converter/RequestUriToElasticSearch.php create mode 100755 src/Converter/RequestUriToSql.php create mode 100644 src/Middleware/Response.php create mode 100644 src/Request/Baka.php create mode 100644 src/Request/Swoole.php create mode 100644 src/Response/Swoole.php create mode 100644 src/Router/Collection.php create mode 100644 storage/db/migrations/20190408003104_ls.php create mode 100644 storage/db/migrations/schema.php create mode 100644 storage/db/seeds/LeadSeeder.php create mode 100755 tests/_bootstrap.php create mode 100755 tests/_data/dump.sql create mode 100755 tests/_output/.gitignore create mode 100755 tests/_support/AcceptanceTester.php create mode 100755 tests/_support/FunctionalTester.php create mode 100755 tests/_support/Helper/Acceptance.php create mode 100755 tests/_support/Helper/Functional.php create mode 100755 tests/_support/Helper/Unit.php create mode 100644 tests/_support/Indices/Leads.php create mode 100644 tests/_support/Model/Leads.php create mode 100755 tests/_support/UnitTester.php create mode 100755 tests/_support/_generated/AcceptanceTesterActions.php create mode 100755 tests/_support/_generated/FunctionalTesterActions.php create mode 100755 tests/_support/_generated/UnitTesterActions.php create mode 100755 tests/acceptance.suite.yml create mode 100755 tests/acceptance/_bootstrap.php create mode 100755 tests/functional.suite.yml create mode 100755 tests/functional/_bootstrap.php create mode 100755 tests/unit.suite.yml create mode 100644 tests/unit/PhalconUnitTestCase.php create mode 100644 tests/unit/UriToElasticSqlTest.php create mode 100644 tests/unit/UriToSqlTest.php create mode 100755 tests/unit/_bootstrap.php diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aea4149 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +//Site Config + +DOMAIN= +URL= +BASE_DIR= +PRODUCTION=0 +CONFIG_CACHE_KEY= + +DEBUG_PROFILE=1 +DEBUG_QUERY=1 + +//DB +DATA_API_MYSQL_HOST=localhost +DATA_API_MYSQL_NAME=bakahttp +DATA_API_MYSQL_USER=root +DATA_API_MYSQL_PASS= + +MEMCACHE_HOST=127.0.0.1 +MEMCACHE_PORT=11211 + +PHINX_CONFIG_DIR=storage/ + +ELASTIC_HOST=127.0.0.1:9200 diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..a79610b --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ +._* +.env +clover.xml +composer.lock +coveralls-upload.json +phpunit.xml +vendor +tests/_output/* \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d9f7fb2 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) MCTekK S.R.L. https://mctekk.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. diff --git a/README.md b/README.md new file mode 100755 index 0000000..64d4ff1 --- /dev/null +++ b/README.md @@ -0,0 +1,315 @@ +# Baka HTTP + +PhalconPHP package to create fast RESTful API's providing a simple way to create fast CRUD's + +## Table of Contents +1. [Testing](#markdown-header-testing) +2. [REST CRUD](#markdown-header-routes) + 1. [Controller Configuration](#markdown-header-controllers) +3. [QueryParser](#markdown-header-QueryParser) +4. [QueryParser Extended](#markdown-header-QueryParser-Extended) + +## Testing +``` +codecept run +``` + +## Routes Configuration + +To avoid having to create Controller for CRUD api we provide the \Baka\Http\Rest\CrudController + +Add to your routes.php + +```php + 'custom-fields', + 'leads', + 'products', + 'productType' => 'product-type', + 'users', + 'sellers', +]; + +$router = new RouterCollection($application); + +foreach ($defaultCrudRoutes as $key => $route) { + + //set the controller name + $name = is_int($key) ? $route : $key; + $controllerName = ucfirst($name) . 'Controller'; + + $router->get('/v1/' . $route, [ + 'Gewaer\Controllers\\' . $controllerName, + 'index', + ]); + + $router->post('/v1/' . $route, [ + 'Gewaer\Controllers\\' . $controllerName, + 'create', + ]); + + $router->get('/v1/' . $route . '/{id}', [ + 'Gewaer\Controllers\\' . $controllerName, + 'getById', + ]); + + $router->put('/v1/' . $route . '/{id}', [ + 'Gewaer\Controllers\\' . $controllerName, + 'edit', + ]); + + $router->delete('/v1/' . $route . '/{id}', [ + 'Gewaer\Controllers\\' . $controllerName, + 'delete', + ]); + + /** + * Mounting routes + */ + $router->mount(); +} +``` + +You can also pass params to the routes to disable JWT and in the future assigne a middleware +```php +setPrefix('/v1'); +$router->get('/', [ + 'Gewaer\Api\Controllers\IndexController', + 'index', + 'options' => [ + 'jwt' => false, + ] +]); +``` + +## Controller configuration + +Add + +```php +model = new Clients(); + $this->customModel = new ClientsCustomFields(); +} +``` + + +# QueryParser + +Parse GET request for a API , giving the user the correct phalcon model params to perform a search + +``` +//search by fieds and specify the list of fields +GET - /v1/?q=(searchField1:value1,searchField2:value2)&fields=id_pct,alias,latitude,longitude,category,chofer,phone,coords,last_report&limit=1&page=2&sort=id_pct|desc + +//filter by relationships +GET - /v1/?q=(searchField1:value1,searchField2:value2)&with=vehicles_media[seriesField:value] + +//add to the array a relationship of this model +GET - /v1/?q=(searchField1:value1,searchField2:value2)&with=vehicles_media[seriesField:value]&relationships=direccione +``` + + +```php +request->getQuery()); +$parse->request(); + +[conditions] => 1 = 1 AND searchField1 = ?1 AND searchField2 = ?2 +[bind] => Array + ( + [1] => value1 + [2] => value2 + ) + +[columns] => Array + ( + [0] => id_pct + [1] => alias + [2] => latitude + [3] => longitude + [4] => category + [5] => chofer + [6] => phone + [7] => coords + [8] => last_report + ) + +[order] => id_pct desc +[limit] => 10 +[offset] => 10 +``` + +# ~~QueryParser CustomFields~~ (DEPRECATED) + +Parse GET request for a API , given the same params as before but with cq (for custom domains) , this give out a normal SQL statement for a raw query + +`GET - /v1/?q=(searchField1:value1,searchField2:value2)&cq=(member_id:1)&q=(leads_status_id:1)` + relationship of this model + + +```php +request->getQuery(); +$parse = new QueryParserCustomFields($request, $this->model); +$params = $parse->request(); +$newRecordList = []; + +$recordList = (new SimpleRecords(null, $this->model, $this->model->getReadConnection()->query($params['sql'], $params['bind']))); + +//navigate los records +$newResult = []; +foreach ($recordList as $key => $record) { + + //field the object + foreach ($record->getAllCustomFields() as $key => $value) { + $record->{$key} = $value; + } + + $newResult[] = $record->toFullArray(); +} + +unset($recordList); +``` + +# QueryParser Extended + +The extended query parser allows you to append search parameters directly via the controller without having to rewrite the function code. + +Features include the ability to search within a model, within a model's custom fields and within a model's descendant relationships. + +Parameters are passed in the format `field` `operator` `value`. Valid operators are `:`, `>`, `<`. + +Multiple fields can be search by separating them with a `,`. You can search a field by several values by separating said values with `|` (equivalent to SQL's `OR`). + +### Query the Model +`GET - /v1/model?q=(field1:value1,field2:value2|value3)` + +### Query the Custom Fields +`GET - /v1/model?cq=(field1>value1)` + +### Query related Models +Querying related models demands a slightly different structure. Each related model that we want queried must be passed as they are named in the system, `_` is used to separate camel cases. + +`GET - /v1/model?rq[model_name]=(field1value1,field1value3,field2value6)` + +_Just remember to escape any special character you want to send through a query string to avoid unwanted results._ + +## Usage +In order to access the extended query parser features your controller has to extend from `CrudExtendedController`. + +```php +additionalSearchFields = [ + ['field', ':', 'value'], + ]; + + return parent::index(); +} +``` + +This method uses the operators that are passed to the query parser via the URL query. Valid operators are (with their SQL equivalents): +```php + '=', + '>' => '>=', + '<' => '<=', +]; +``` + +# API Custom Fields CRUD + +The CRUD handles the default behavior: +- GET /v1/leads -> get all +- GET /v1/leads/1 -> get one +- POST /v1/leads -> create +- PUT /v1/leads/1 -> update +- DELETE /v1/leads/1 -> delete + +In other to use the custom fields you need to extend you controller from CrudCustomFieldsController and define the method `onConstruct()` on this method you define the model of the custom field and the model of the value of this custom fields + +```php +model = new Leads(); + $this->customModel = new LeadsCustomFields(); +} +``` + +Thats it, your controller now manages the custom fields as if they wher properties of the main class + +# Normal API CRUD + +Just extend your API controller from CrudController and you will have the following functions: + +The CRUD handles the default behaviero: +- GET /v1/leads -> get all +- GET /v1/leads/1 -> get one +- POST /v1/leads -> create +- PUT /v1/leads/1 -> update +- DELETE /v1/leads/1 -> delete + +createFields and updateFields are needed to be define in other to create the field diff --git a/codeception.yml b/codeception.yml new file mode 100755 index 0000000..3a8fca8 --- /dev/null +++ b/codeception.yml @@ -0,0 +1,21 @@ +actor: Tester +paths: + tests: tests + log: tests/_output + data: tests/_data + support: tests/_support + envs: tests/_envs +settings: + bootstrap: _bootstrap.php + colors: true + memory_limit: 1024M +extensions: + enabled: + - Codeception\Extension\RunFailed +modules: + config: + Db: + dsn: '' + user: '' + password: '' + dump: tests/_data/dump.sql diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..d511220 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "baka/http", + "description": "Baka Http component", + "license": "MIT", + "authors": [{ + "name": "kaioken", + "email": "max@mctekk.com" + }], + "require": { + "php": ">=7.2", + "ext-phalcon": ">=3.0.0", + "vlucas/phpdotenv": "^2.0", + "phalcon/incubator": ">=3.0.0", + "baka/database": "^0.5", + "baka/elasticsearch": "^1.0", + "elasticsearch/elasticsearch": "^6.1", + "guzzlehttp/guzzle": "^6.3" + }, + "require-dev": { + "codeception/codeception": "^2.4", + "codeception/verify": "*", + "vlucas/phpdotenv": "^2.0", + "phalcon/incubator": "~3.3", + "odan/phinx-migrations-generator": "^4.0" + }, + "autoload": { + "psr-4": { + "Baka\\Http\\": "" + } + }, + "autoload-dev": { + "psr-4": { + "Baka\\Http\\": "" + } + } +} diff --git a/phinx.php b/phinx.php new file mode 100644 index 0000000..f7d51ac --- /dev/null +++ b/phinx.php @@ -0,0 +1,33 @@ +load(); + +return [ + 'paths' => [ + 'migrations' => getenv('PHINX_CONFIG_DIR') . '/db/migrations', + 'seeds' => getenv('PHINX_CONFIG_DIR') . '/db/seeds', + ], + 'environments' => [ + 'default_migration_table' => 'ut_migrations', + 'default_database' => 'development', + 'production' => [ + 'adapter' => 'mysql', + 'host' => getenv('DATA_API_MYSQL_HOST'), + 'name' => getenv('DATA_API_MYSQL_NAME'), + 'user' => getenv('DATA_API_MYSQL_USER'), + 'pass' => getenv('DATA_API_MYSQL_PASS'), + 'port' => 3306, + 'charset' => 'utf8', + ], + 'development' => [ + 'adapter' => 'mysql', + 'host' => getenv('DATA_API_MYSQL_HOST'), + 'name' => getenv('DATA_API_MYSQL_NAME'), + 'user' => getenv('DATA_API_MYSQL_USER'), + 'pass' => getenv('DATA_API_MYSQL_PASS'), + 'port' => 3306, + 'charset' => 'utf8', + ], + ], + 'version_order' => 'creation', +]; diff --git a/phpcs.xml b/phpcs.xml new file mode 100755 index 0000000..9f4429f --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,57 @@ + + + Phalcon Coding Standards + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tests/cli + tests/integration + tests/unit + tests/_data/fixtures/Traits + tests/_support/Helper + \ No newline at end of file diff --git a/src/Api/BaseController.php b/src/Api/BaseController.php new file mode 100644 index 0000000..c8fe897 --- /dev/null +++ b/src/Api/BaseController.php @@ -0,0 +1,122 @@ + $statusCode, + 'statusMessage' => $statusMessage, + 'content' => $content, + ]; + + if ($this->config->application->debug->logRequest) { + $this->log->addInfo('RESPONSE', $response); + } + + //in order to use the current response instead of having to create a new object , this is needed for swoole servers + //$response = $this->response ?? new Response(); + $this->response->setStatusCode($statusCode, $statusMessage); + $this->response->setContentType('application/vnd.api+json', 'UTF-8'); + $this->response->setJsonContent($content); + + return $this->response; + } +} diff --git a/src/Api/CrudCustomFieldsController.php b/src/Api/CrudCustomFieldsController.php new file mode 100644 index 0000000..d7f55f7 --- /dev/null +++ b/src/Api/CrudCustomFieldsController.php @@ -0,0 +1,205 @@ +getById($id); + } + + //parse the rquest + $parse = new QueryParserCustomFields($this->request->getQuery(), $this->model); + $parse->appendParams($this->additionalSearchFields); + $parse->appendCustomParams($this->additionalCustomSearchFields); + $parse->appendRelationParams($this->additionalRelationSearchFields); + $params = $parse->request(); + + $results = (new SimpleRecords(null, $this->model, $this->model->getReadConnection()->query($params['sql'], $params['bind']))); + $count = $this->model->getReadConnection()->query($params['countSql'], $params['bind'])->fetch(\PDO::FETCH_OBJ)->total; + $relationships = false; + + // Relationships, but we have to change it to sparo full implementation + if ($this->request->hasQuery('relationships')) { + $relationships = $this->request->getQuery('relationships', 'string'); + } + + //navigate los records + $newResult = []; + foreach ($results as $key => $record) { + //field the object + foreach ($record->getAllCustomFields() as $key => $value) { + $record->{$key} = $value; + } + + $newResult[] = !$relationships ? $record->toFullArray() : QueryParserCustomFields::parseRelationShips($relationships, $record); + } + + unset($results); + + //this means the want the response in a vuejs format + if ($this->request->hasQuery('format')) { + $limit = (int)$this->request->getQuery('limit', 'int', 25); + + $newResult = [ + 'data' => $newResult, + 'limit' => $limit, + 'page' => $this->request->getQuery('page', 'int', 1), + 'total_pages' => ceil($count / $limit) + ]; + } + + return $this->response($newResult); + } + + /** + * Get item. + * + * @method GET + * url /v1/controller/{id} + * + * @param mixed $id + * + * @return \Phalcon\Http\Response + * @throws \Exception + */ + public function getById($id): Response + { + //find the info + $record = $this->model->findFirst($id); + + if (!is_object($record)) { + throw new UnprocessableEntityHttpException('Record not found'); + } + + $relationships = false; + + //get relationship + if ($this->request->hasQuery('relationships')) { + $relationships = $this->request->getQuery('relationships', 'string'); + } + + $result = !$relationships ? $record->toFullArray() : QueryParserCustomFields::parseRelationShips($relationships, $record); + + return $this->response($result); + } + + /** + * Add a new item. + * + * @method POST + * url /v1/controller + * + * @return \Phalcon\Http\Response + * @throws \Exception + */ + public function create(): Response + { + $request = $this->request->getPost(); + + if (empty($request)) { + $request = $this->request->getJsonRawBody(true); + } + + //we need even if empty the custome fields + if (empty($request)) { + throw new Exception('No valie info sent'); + } + + //set the custom fields to update + $this->model->setCustomFields($request); + + //try to save all the fields we allow + if ($this->model->save($request, $this->createFields)) { + return $this->getById($this->model->id); + } else { + //if not thorw exception + throw new Exception($this->model->getMessages()[0]); + } + } + + /** + * Update an item. + * + * @method PUT + * url /v1/controller/{id} + * + * @param mixed $id + * + * @return \Phalcon\Http\Response + * @throws \Exception + */ + public function edit($id): Response + { + if ($objectInfo = $this->model->findFirst($id)) { + $request = $this->request->getPut(); + + if (empty($request)) { + $request = $this->request->getJsonRawBody(true); + } + + if (empty($request)) { + throw new Exception('No valid data sent.'); + } + + //set the custom fields to update + $objectInfo->setCustomFields($request); + + //update + if ($objectInfo->update($request, $this->updateFields)) { + return $this->getById($id); + } else { + //didnt work + throw new Exception($objectInfo->getMessages()[0]); + } + } else { + throw new Exception(_('Record not found')); + } + } + + /** + * Delete an item. + * + * @method DELETE + * url /v1/controller/{id} + * + * @param mixed $id + * + * @return \Phalcon\Http\Response + * @throws \Exception + */ + public function delete($id): Response + { + if ($objectInfo = $this->model->findFirst($id)) { + if ($objectInfo->delete() === false) { + foreach ($objectInfo->getMessages() as $message) { + throw new Exception($message); + } + } + + return $this->response(['Delete Successfully']); + } else { + throw new Exception(_('Record not found')); + } + } +} diff --git a/src/Contracts/Api/CrudBehaviorTrait.php b/src/Contracts/Api/CrudBehaviorTrait.php new file mode 100644 index 0000000..b253f4f --- /dev/null +++ b/src/Contracts/Api/CrudBehaviorTrait.php @@ -0,0 +1,280 @@ +getQuery(), $this->model); + $parse->setCustomColumns($this->customColumns); + $parse->setCustomTableJoins($this->customTableJoins); + $parse->setCustomConditions($this->customConditions); + $parse->setCustomLimit($this->customLimit); + $parse->setCustomSort($this->customSort); + $parse->appendParams($this->additionalSearchFields); + $parse->appendCustomParams($this->additionalCustomSearchFields); + $parse->appendRelationParams($this->additionalRelationSearchFields); + + //conver to SQL + return $parse->convert(); + } + + /** + * Given the results we append the relationships. + * + * @param RequestInterface $request + * @param array|object $results + * @return array + */ + protected function appendRelationshipsToResult(RequestInterface $request, $results) + { + // Relationships, but we have to change it to sparo full implementation + if ($request->hasQuery('relationships')) { + $relationships = $request->getQuery('relationships', 'string'); + + $results = RequestUriToSql::parseRelationShips($relationships, $results); + } + + return $results; + } + + /** + * Given the results we will proess the output + * we will check if a DTO transformer exist and if so we will send it over to change it. + * + * @param object|array $results + * @return void + */ + protected function processOutput($results) + { + return $results; + } + + /** + * Given a array request from a method DTO transformet to whats is needed to + * process it. + * + * @param array $request + * @return array + */ + protected function processInput(array $request): array + { + return $request; + } + + // TODO: Move it to its own class. + + /** + * Given a process request return the records. + * + * @return void + */ + protected function getRecords(array $processedRequest): array + { + // TODO: Create a const with these values + $required = ['sql', 'countSql', 'bind']; + + if ($diff = array_diff($required, array_keys($processedRequest))) { + throw new ArgumentCountError( + sprintf( + 'Request no processed. Missing following params : %s.', + implode(', ', $diff) + ) + ); + } + + $results = new SimpleRecords( + null, + $this->model, + $this->model->getReadConnection()->query($processedRequest['sql'], $processedRequest['bind']) + ); + + $count = $this->model->getReadConnection()->query( + $processedRequest['countSql'], + $processedRequest['bind'] + )->fetch(PDO::FETCH_OBJ)->total; + + return [ + 'results' => $results, + 'total' => $count + ]; + } + + /** + * Given the model list the records based on the filter. + * + * @return Response + */ + public function index(): Response + { + $results = $this->processIndex(); + //return the response + transform it if needed + return $this->response($results); + } + + /** + * body of the index function to simply extending methods. + * + * @return void + */ + protected function processIndex() + { + //conver the request to sql + $processedRequest = $this->processRequest($this->request); + $records = $this->getRecords($processedRequest); + + //get the results and append its relationships + $results = $this->appendRelationshipsToResult($this->request, $records['results']); + + //this means the want the response in a vuejs format + if ($this->request->hasQuery('format')) { + $limit = (int) $this->request->getQuery('limit', 'int', 25); + + $results = [ + 'data' => $results, + 'limit' => $limit, + 'page' => $this->request->getQuery('page', 'int', 1), + 'total_pages' => ceil($records['total'] / $limit), + ]; + } + + return $this->processOutput($results); + } + + /** + * Get the record by its primary key. + * + * @param mixed $id + * + * @throws Exception + * @return Response + */ + public function getById($id): Response + { + //find the info + $record = $this->model::findFirstOrFail([ + 'conditions' => $this->model->getPrimaryKey() . '= ?0', + 'bind' => [$id] + ]); + + //get the results and append its relationships + $result = $this->appendRelationshipsToResult($this->request, $record); + + return $this->response($this->processOutput($result)); + } + + /** + * Create new record. + * + * @return Response + */ + public function create(): Response + { + //process the input + $result = $this->processCreate($this->request); + + return $this->response($this->processOutput($result)); + } + + /** + * Process the create request and trecurd the boject. + * + * @return ModelInterface + * @throws Exception + */ + protected function processCreate(RequestInterface $request): ModelInterface + { + //process the input + $request = $this->processInput($request->getPostData()); + + $this->model->saveOrFail($request, $this->createFields); + + return $this->model; + } + + /** + * Update a record. + * + * @param mixed $id + * @return Response + */ + public function edit($id): Response + { + $record = $this->model::findFirstOrFail([ + 'conditions' => $this->model->getPrimaryKey() . '= ?0', + 'bind' => [$id] + ]); + + //process the input + $result = $this->processEdit($this->request, $record); + + return $this->response($this->processOutput($result)); + } + + /** + * Process the update request and return the object. + * + * @param RequestInterface $request + * @param ModelInterface $record + * @throws Exception + * @return ModelInterface + */ + protected function processEdit(RequestInterface $request, ModelInterface $record): ModelInterface + { + //process the input + $request = $this->processInput($request->getPutData()); + + $record->updateOrFail($request, $this->updateFields); + + return $record; + } + + /** + * Delete a Record. + * + * @throws Exception + * @return Response + */ + public function delete($id): Response + { + $record = $this->model::findFirstOrFail([ + 'conditions' => $this->model->getPrimaryKey() . '= ?0', + 'bind' => [$id] + ]); + + if ($this->softDelete == 1) { + $record->softDelete(); + } else { + $record->delete(); + } + + return $this->response(['Delete Successfully']); + } +} diff --git a/src/Contracts/Api/CrudCustomFieldsBehaviorTrait.php b/src/Contracts/Api/CrudCustomFieldsBehaviorTrait.php new file mode 100644 index 0000000..f032616 --- /dev/null +++ b/src/Contracts/Api/CrudCustomFieldsBehaviorTrait.php @@ -0,0 +1,130 @@ +hasQuery('relationships')) { + $relationships = $request->getQuery('relationships', 'string'); + + $results = is_object($results) ? RequestUriToElasticSearch::parseRelationShips($relationships, $results) : $results; + } + + return $results; + } + + /** + * Process output + * + * @param mixed $results + * @return mixed + */ + protected function processOutput($results) + { + return is_object($results) ? $results->toFullArray() : $results; + } + + /** + * Process the create request and trecurd the boject. + * + * @return ModelInterface + * @throws Exception + */ + protected function processCreate(RequestInterface $request): ModelInterface + { + //set the custom fields to create + $this->model->setCustomFields($request->getPostData()); + + $this->processCreateParent($request); + + return $this->model; + } + + /** + * Process the update request and return the object. + * + * @param RequestInterface $request + * @param ModelInterface $record + * @throws Exception + * @return ModelInterface + */ + protected function processEdit(RequestInterface $request, ModelInterface $record): ModelInterface + { + //set the custom fields to update + $record->setCustomFields($request->getPutData()); + + $record = $this->processEditParent($request, $record); + + return $record; + } + + /** + * Given a process request return the records. + * + * @return void + */ + protected function getRecords(array $processedRequest): array + { + $required = ['sql', 'countSql', 'bind']; + + if (count(array_intersect_key(array_flip($required), $processedRequest)) != count($required)) { + throw new ArgumentCountError('Not a processed request missing any of the following params : SQL, CountSQL, Bind'); + } + + $results = new SimpleRecords( + null, + $this->model, + $this->model->getReadConnection()->query($processedRequest['sql'], $processedRequest['bind']) + ); + + $count = $this->model->getReadConnection()->query( + $processedRequest['countSql'], + $processedRequest['bind'] + )->fetch(PDO::FETCH_OBJ)->total; + + //navigate los records + $newResult = []; + $relationships = $this->request->getQuery('relationships', 'string'); + + foreach ($results as $key => $record) { + //field the object + foreach ($record->getAllCustomFields() as $key => $value) { + $record->{$key} = $value; + } + + /** + * @todo clean this up later on regarding custom fields SQL + */ + $newResult[] = !$relationships ? $record->toFullArray() : RequestUriToElasticSearch::parseRelationShips($relationships, $record); + } + + unset($results); + + return [ + 'results' => $newResult, + 'total' => $count + ]; + } +} diff --git a/src/Contracts/Api/CrudElasticBehaviorTrait.php b/src/Contracts/Api/CrudElasticBehaviorTrait.php new file mode 100644 index 0000000..ac26f8f --- /dev/null +++ b/src/Contracts/Api/CrudElasticBehaviorTrait.php @@ -0,0 +1,67 @@ +getQuery(), $this->model); + $parse->setCustomColumns($this->customColumns); + $parse->setCustomTableJoins($this->customTableJoins); + $parse->setCustomConditions($this->customConditions); + $parse->appendParams($this->additionalSearchFields); + $parse->appendCustomParams($this->additionalCustomSearchFields); + $parse->appendRelationParams($this->additionalRelationSearchFields); + + //conver to SQL + return $parse->convert(); + } + + /** + * Given a process request return the records. + * + * @return void + */ + protected function getRecords(array $processedRequest): array + { + $required = ['sql', 'countSql', 'bind']; + + if (count(array_intersect_key(array_flip($required), $processedRequest)) != count($required)) { + throw new ArgumentCountError('Not a processed request missing any of the following params : SQL, CountSQL, Bind'); + } + + $client = new Client('http://' . current($this->config->elasticSearch['hosts'])); + $results = $client->findBySql($processedRequest['sql']); + + return [ + 'results' => $results, + 'total' => 0 //@todo fix this + ]; + } +} diff --git a/src/Contracts/Converter/ConverterInterface.php b/src/Contracts/Converter/ConverterInterface.php new file mode 100644 index 0000000..bdefe33 --- /dev/null +++ b/src/Contracts/Converter/ConverterInterface.php @@ -0,0 +1,13 @@ +customColumns = ' ,' . $query; + } + } + + /** + * Set the custom table by the user + * you can do inner joins or , table . If you are just adding a table you will need to specify the ,. + * + * @param string $query + * @return void + */ + public function setCustomTableJoins(?string $query) : void + { + if (!is_null($query)) { + $this->customTableJoins = ' ' . $query; + } + } + + /** + * set custom conditions for the query , need to start with and AND or OR. + * + * @param string $query + * @return void + */ + public function setCustomConditions(?string $query) : void + { + if (!is_null($query)) { + $this->customConditions = ' ' . $query; + } + } + + /** + * Overwrite the limit of the current Request. + * + * @param integer $limit + * @return void + */ + public function setCustomLimit(?int $limit): void + { + if (!is_null($limit)) { + $this->limit = $limit; + } + } +} diff --git a/src/Converter/RequestUriToElasticSearch.php b/src/Converter/RequestUriToElasticSearch.php new file mode 100644 index 0000000..b7ac390 --- /dev/null +++ b/src/Converter/RequestUriToElasticSearch.php @@ -0,0 +1,505 @@ + '=', + '>' => '>=', + '<' => '<=', + '~' => '!=', + ]; + + /** + * Pass the request. + */ + public function __construct(array $request, Model $model) + { + $this->request = $request; + $this->model = $model; + } + + /** + * Main method for parsing a query string. + * Finds search paramters, partial response fields, limits, and offsets. + * Sets Controller fields for these variables. + * + * @param array $allowedFields Allowed fields array for search and partials + * @return boolean Always true if no exception is thrown + */ + public function convert(): array + { + $params = [ + 'subquery' => '', + ]; + + $hasSubquery = false; + + //if we find that we are using custom field this is a different beast so we have to send it + //to another functino to deal with this shit + if (array_key_exists('cq', $this->request)) { + $params['cparams'] = $this->request['cq']; + } + + //verify the user is searching for something + if (array_key_exists('q', $this->request)) { + $params['params'] = $this->request['q']; + } + + // Check to see if the user wants certain columns returned + if (array_key_exists('columns', $this->request)) { + $this->parseColumns($this->request['columns']); + } else { + $this->columns = '*'; + } + + // Check the limit the user is asking for. + if (array_key_exists('limit', $this->request)) { + $limit = (int) $this->request['limit']; + // Prevent ridiculous limits. Nothing above 200 and nothing below 1. + if ($limit >= 1 && $limit <= 200) { + $this->limit = $limit; + } elseif ($limit > 200) { + $this->limit = 200; + } elseif ($limit < 1) { + $this->limit = 25; + } + } + + // Check the page the user is asking for. + if (array_key_exists('page', $this->request)) { + $page = (int) $this->request['page']; + // Prevent ridiculous pagination requests + if ($page >= 1) { + $this->page = $page; + } + } + + // Sorting logic for related searches. + if (array_key_exists('sort', $this->request)) { + if (!empty($this->request['sort'])) { + $this->setCustomSort(trim($this->request['sort'])); + } + } + + // Prepare the search parameters. + $this->prepareParams($params); + + // Append any additional user parameters + $this->appendAdditionalParams(); + //base on th eesarch params get the raw query + $rawSql = $this->prepareCustomSearch(); + + if (!is_null($this->sort)) { + $rawSql['sql'] .= $this->sort; + } + + // Calculate the corresponding offset + $this->offset = ($this->page - 1) * $this->limit; + $rawSql['sql'] .= " LIMIT {$this->limit} OFFSET {$this->offset}"; + + return $rawSql; + } + + /** + * gien the request array , get the custom query to find the results. + * + * @param array $params + * @return string + */ + protected function prepareCustomSearch($hasSubquery = false): array + { + $metaData = new \Phalcon\Mvc\Model\MetaData\Memory(); + $classReflection = (new \ReflectionClass($this->model)); + $classname = $this->model->getSource(); + + $primaryKey = null; + + if ($primaryKey = $metaData->getPrimaryKeyAttributes($this->model)) { + $primaryKey = $primaryKey[0]; + } + + $sql = ''; + + $sql .= ' WHERE'; + + // create normal sql search + if (!empty($this->normalSearchFields)) { + foreach ($this->normalSearchFields as $fKey => $searchFieldValues) { + if (is_array(current($searchFieldValues))) { + foreach ($searchFieldValues as $csKey => $chainSearch) { + $sql .= !$csKey ? ' AND (' : ''; + $sql .= $this->prepareNormalSql($chainSearch, $classname, ($csKey ? 'OR' : ''), $fKey); + $sql .= ($csKey == count($searchFieldValues) - 1) ? ') ' : ''; + } + } else { + $sql .= $this->prepareNormalSql($searchFieldValues, $classname, 'AND', $fKey); + } + } + } + + // create custom query sql + if (!empty($this->customSearchFields)) { + // print_r($this->customSearchFields);die(); + // We have to pre-process the fields in order to have them bundled together. + $customSearchFields = []; + + foreach ($this->customSearchFields as $fKey => $searchFieldValues) { + if (is_array(current($searchFieldValues))) { + foreach ($searchFieldValues as $csKey => $chainSearch) { + $searchTable = explode('.', $chainSearch[0])[0]; + $customSearchFields[$fKey][$searchTable][] = $chainSearch; + } + } else { + $searchTable = explode('.', $searchFieldValues[0])[0]; + $customSearchFields[$searchTable][] = $searchFieldValues; + } + } + + // print_r($customSearchFields);die(); + + $prepareNestedSql = function (array $searchCriteria, string $classname, string $andOr, string $fKey): string { + $sql = ''; + $textFields = $this->getTextFields($classname); + list($searchField, $operator, $searchValues) = $searchCriteria; + $operator = $this->operators[$operator]; + + if (trim($searchValues) !== '') { + if ($searchValues == '%%') { + $sql .= ' ' . $andOr . ' (' . $searchField . ' IS NULL'; + $sql .= ' OR ' . $searchField . ' = ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $searchField . ' = 0'; + } + + $sql .= ')'; + } elseif ($searchValues == '$$') { + $sql .= ' ' . $andOr . ' (' . $searchField . ' IS NOT NULL'; + $sql .= ' OR ' . $searchField . ' != ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $searchField . ' ) != 0'; + } + + $sql .= ')'; + } else { + if (strpos($searchValues, '|')) { + $searchValues = explode('|', $searchValues); + } else { + $searchValues = [$searchValues]; + } + + $sqlArray = []; + foreach ($searchValues as $vKey => $value) { + if ((preg_match('#^%[^%]+%|%[^%]+|[^%]+%$#i', $value)) + || $value == '%%' + ) { + $operator = 'LIKE'; + } + + if (!$vKey) { + $sql .= ' ' . $andOr . ' (' . $searchField . ' ' . $operator . ' :f' . $searchField . $fKey . $vKey; + } else { + $sql .= ' OR ' . $searchField . ' ' . $operator . ' :f' . $searchField . $fKey . $vKey; + } + + $this->bindParamsKeys[] = ':f' . $searchField . $fKey . $vKey; + $this->bindParamsValues[] = "'{$value}'"; + } + + $sql .= ')'; + } + } + + return $sql; + }; + + // With the stuff processed we now proceed to assemble the query + + foreach ($customSearchFields as $fKey => $searchFieldValues) { + // If the key is an integer, this means the fields have to be OR'd inside the nesting + if (is_int($fKey)) { + $nestedSql = ' AND ('; + $first = true; + foreach ($searchFieldValues as $csKey => $chainSearch) { + if (count($chainSearch) > 1) { + $nestedSql .= ' nested("' . $csKey . '",'; + foreach ($chainSearch as $cKey => $chain) { + $nestedSql .= $prepareNestedSql($chain, $classname, ($cKey ? 'OR' : ''), $csKey . $cKey); + } + $nestedSql .= ') '; + } else { + $nestedSql .= !$first ? ' OR nested("' . $csKey . '",' : ' nested("' . $csKey . '",'; + $nestedSql .= $prepareNestedSql($chainSearch[0], $classname, '', $csKey); + $nestedSql .= ') '; + } + $first = false; + } + $sql .= $nestedSql . ') '; + } else { + $nestedSql = ' AND nested("' . $fKey . '",'; + foreach ($searchFieldValues as $csKey => $chainSearch) { + $nestedSql .= $prepareNestedSql($chainSearch, $classname, ($csKey ? 'AND' : ''), $fKey); + } + $nestedSql .= ') '; + $sql .= $nestedSql; + } + } + + // ================================================== + // ================================================== + // ================================================== + + // foreach ($this->customSearchFields as $fKey => $searchFieldValues) { + // if (is_array(current($searchFieldValues))) { + // foreach ($searchFieldValues as $csKey => $chainSearch) { + // $sql .= !$csKey ? ' AND (' : ''; + // $sql .= $this->prepareNestedSql($chainSearch, $classname, ($csKey ? 'OR' : ''), $fKey); + // $sql .= ($csKey == count($searchFieldValues) - 1) ? ') ' : ''; + // } + // } else { + // $sql .= $this->prepareNestedSql($searchFieldValues, $classname, 'AND', $fKey); + // } + // } + } + + // Replace initial `AND ` or `OR ` to avoid SQL errors. + $sql = str_replace( + ['WHERE AND', 'WHERE OR', 'WHERE ( OR'], + ['WHERE', 'WHERE', 'WHERE ('], + $sql + ); + + // Remove empty where from the end of the string. + $sql = preg_replace('# WHERE$#', '', $sql); + + //sql string + $countSql = 'SELECT COUNT(*) total FROM ' . $classname . $this->customTableJoins . $sql . $this->customConditions; + $resultsSql = "SELECT {$this->columns} {$this->customColumns} FROM {$classname} {$this->customTableJoins} {$sql} {$this->customConditions}"; + //bind params + $bindParams = array_combine($this->bindParamsKeys, $this->bindParamsValues); + + return [ + 'sql' => strtr($resultsSql, $bindParams), + 'countSql' => strtr($countSql, $bindParams), + 'bind' => null, + ]; + } + + /** + * Prepare the SQL for a normal search. + * + * @param array $searchCriteria + * @param string $classname + * @param string $andOr + * @param int $fKey + * + * @return string + */ + protected function prepareNormalSql(array $searchCriteria, string $classname, string $andOr, int $fKey): string + { + $sql = ''; + $textFields = $this->getTextFields($classname); + list($searchField, $operator, $searchValues) = $searchCriteria; + $operator = $this->operators[$operator]; + + if (trim($searchValues) !== '') { + if ($searchValues == '%%') { + $sql .= ' ' . $andOr . ' (' . $searchField . ' IS NULL'; + $sql .= ' OR ' . $searchField . ' = ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $searchField . ' = 0'; + } + + $sql .= ')'; + } elseif ($searchValues == '$$') { + $sql .= ' ' . $andOr . ' (' . $searchField . ' IS NOT NULL'; + $sql .= ' OR ' . $searchField . ' != ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $searchField . ' != 0'; + } + + $sql .= ')'; + } else { + if (strpos($searchValues, '|')) { + $searchValues = explode('|', $searchValues); + } else { + $searchValues = [$searchValues]; + } + + foreach ($searchValues as $vKey => $value) { + if (preg_match('#^%[^%]+%|%[^%]+|[^%]+%$#i', $value) + || $value == '%%' + ) { + $operator = 'LIKE'; + } + + if (!$vKey) { + $sql .= ' ' . $andOr . ' (' . $searchField . ' ' . $operator . ' :f' . $searchField . $fKey . $vKey; + } else { + $sql .= ' OR ' . $searchField . ' ' . $operator . ' :f' . $searchField . $fKey . $vKey; + } + + $this->bindParamsKeys[] = ':f' . $searchField . $fKey . $vKey; + $this->bindParamsValues[] = "'{$value}'"; + } + + $sql .= ')'; + } + } + + return $sql; + } + + /** + * Prepare the SQL for a related search. + * + * @param array $searchCriteria + * @param string $classname + * @param string $andOr + * @param int $fKey + * + * @return string + */ + protected function prepareNestedSql(array $searchCriteria, string $classname, string $andOr, string $fKey): string + { + $sql = ''; + $textFields = $this->getTextFields($classname); + $nested = ' nested('; + list($searchField, $operator, $searchValues) = $searchCriteria; + $operator = $this->operators[$operator]; + + if (trim($searchValues) !== '') { + if ($searchValues == '%%') { + $sql .= ' ' . $andOr . ' (' . $nested . '' . $searchField . ' IS NULL'; + $sql .= ' OR ' . $nested . '' . $searchField . ' = ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $nested . '' . $searchField . ' = 0'; + } + + $sql .= ')'; + } elseif ($searchValues == '$$') { + $sql .= ' ' . $andOr . ' (' . $nested . '' . $searchField . ' IS NOT NULL'; + $sql .= ' OR ' . $nested . '' . $searchField . ' != ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $nested . '' . $searchField . ' ) != 0'; + } + + $sql .= ')'; + } else { + if (strpos($searchValues, '|')) { + $searchValues = explode('|', $searchValues); + } else { + $searchValues = [$searchValues]; + } + + foreach ($searchValues as $vKey => $value) { + if (preg_match('#^%[^%]+%|%[^%]+|[^%]+%$#i', $value) + || $value == '%%' + ) { + $operator = 'LIKE'; + } + + if (!$vKey) { + $sql .= ' ' . $andOr . ' (' . $nested . '' . $searchField . ') ' . $operator . ' :f' . $searchField . $fKey . $vKey; + } else { + $sql .= ' OR ' . $nested . '' . $searchField . ' ' . $operator . ' :f' . $searchField . $fKey . $vKey; + } + + $this->bindParamsKeys[] = ':f' . $searchField . $fKey . $vKey; + $this->bindParamsValues[] = $value; + } + + $sql .= ')'; + } + } + + return $sql; + } + + /** + * Preparse the parameters to be used in the search. + * + * @return void + */ + protected function prepareParams(array $unparsed): void + { + $this->customSearchFields = array_key_exists('cparams', $unparsed) ? $this->parseSearchParameters($unparsed['cparams'])['mapped'] : []; + $this->normalSearchFields = array_key_exists('params', $unparsed) ? $this->parseSearchParameters($unparsed['params'])['mapped'] : []; + } + + /** + * Parse the requested columns to be returned. + * + * @param string $columns + * + * @return void + */ + protected function parseColumns(string $columns): void + { + // Split the columns string into individual columns + $columns = explode(',', $columns); + + foreach ($columns as &$column) { + $column = preg_replace('/[^a-zA-_Z]/', '', $column); + if (strpos($column, '.') === false) { + $column = "{$column}"; + } else { + $as = str_replace('.', '_', $column); + $column = "{$column} {$as}"; + } + } + + $this->columns = implode(', ', $columns); + } + + /** + * Based on the given relaitonship , add the relation array to the Resultset. + * + * @param string $relationships + * @param Model $results + * @return array + */ + public static function parseRelationShips(string $relationships, &$results) : array + { + $relationships = explode(',', $relationships); + $newResults = []; + if (!($results instanceof Model)) { + throw new Exception(_('Result needs to be a Baka Model')); + } + $newResults = $results->toFullArray(); + foreach ($relationships as $relationship) { + if ($results->$relationship) { + $callRelationship = 'get' . ucfirst($relationship); + $newResults[$relationship] = $results->$callRelationship(); + } + } + unset($results); + return $newResults; + } +} diff --git a/src/Converter/RequestUriToSql.php b/src/Converter/RequestUriToSql.php new file mode 100755 index 0000000..cdcc290 --- /dev/null +++ b/src/Converter/RequestUriToSql.php @@ -0,0 +1,987 @@ + '=', + '>' => '>=', + '<' => '<=', + '~' => '!=', + ]; + + /** + * @var array + */ + protected $bindParamsKeys = []; + + /** + * @var array + */ + protected $bindParamsValues = []; + + /** + * Pass the request. + */ + public function __construct(array $request, Model $model) + { + $this->request = $request; + $this->model = $model; + } + + /** + * Main method for parsing a query string. + * Finds search paramters, partial response fields, limits, and offsets. + * Sets Controller fields for these variables. + * + * @param array $allowedFields Allowed fields array for search and partials + * @return boolean Always true if no exception is thrown + */ + public function convert(): array + { + $params = [ + 'subquery' => '', + ]; + + $hasSubquery = false; + + // Check to see if the user is trying to query a relationship + if (array_key_exists('rq', $this->request)) { + $params['rparams'] = $this->request['rq']; + } + + //if we find that we are using custom field this is a different beast so we have to send it + //to another functino to deal with this shit + if (array_key_exists('cq', $this->request)) { + $params['cparams'] = $this->request['cq']; + } + + //verify the user is searching for something + if (array_key_exists('q', $this->request)) { + $params['params'] = $this->request['q']; + } + + // Check to see if the user wants certain columns returned + if (array_key_exists('columns', $this->request)) { + $this->parseColumns($this->request['columns']); + } else { + $this->columns = "{$this->model->getSource()}.*"; + } + + // Check the limit the user is asking for. + if (array_key_exists('limit', $this->request)) { + $limit = (int) $this->request['limit']; + // Prevent ridiculous limits. Nothing above 200 and nothing below 1. + if ($limit >= 1 && $limit <= 200) { + $this->limit = $limit; + } elseif ($limit > 200) { + $this->limit = 200; + } elseif ($limit < 1) { + $this->limit = 25; + } + } + + // Check the page the user is asking for. + if (array_key_exists('page', $this->request)) { + $page = (int) $this->request['page']; + // Prevent ridiculous pagination requests + if ($page >= 1) { + $this->page = $page; + } + } + + // Sorting logic for related searches. + if (array_key_exists('sort', $this->request)) { + if (!empty($this->request['sort'])) { + $this->setCustomSort(trim($this->request['sort'])); + } + } + + // Prepare the search parameters. + $this->prepareParams($params); + + // Append any additional user parameters + $this->appendAdditionalParams(); + //base on th eesarch params get the raw query + $rawSql = $this->prepareCustomSearch(); + + if (!is_null($this->sort)) { + $rawSql['sql'] .= $this->sort; + } + + // Calculate the corresponding offset + $this->offset = ($this->page - 1) * $this->limit; + $rawSql['sql'] .= " LIMIT {$this->limit} OFFSET {$this->offset}"; + + return $rawSql; + } + + /** + * gien the request array , get the custom query to find the results. + * + * @param array $params + * @return string + */ + protected function prepareCustomSearch($hasSubquery = false): array + { + $metaData = new MetaDataMemory(); + $classReflection = (new ReflectionClass($this->model)); + $classname = $this->model->getSource(); + + $primaryKey = null; + + if ($primaryKey = $metaData->getPrimaryKeyAttributes($this->model)) { + $primaryKey = $primaryKey[0]; + } + + $customClassname = $classname . '_custom_fields'; + $bindParamsKeys = []; + $bindParamsValues = []; + + $sql = ''; + + if (!empty($this->relationSearchFields)) { + foreach ($this->relationSearchFields as $model => $searchFields) { + $modelObject = new $model(); + $model = $modelObject->getSource(); + + $relatedKey = $metaData->getPrimaryKeyAttributes($modelObject)[0]; + $relation = $this->model->getModelsManager()->getRelationsBetween(get_class($this->model), get_class($modelObject)); + $relationKey = (isset($relation) && count($relation)) ? $relation[0]->getFields() : $relatedKey; + + $sql .= " INNER JOIN {$model} ON {$model}.{$relatedKey} = ("; + $sql .= "SELECT {$model}.{$relatedKey} FROM {$model} WHERE {$model}.{$relatedKey} = {$classname}.{$relationKey}"; + + foreach ($searchFields as $fKey => $searchFieldValues) { + if (is_array(current($searchFieldValues))) { + foreach ($searchFieldValues as $csKey => $chainSearch) { + $sql .= !$csKey ? ' (' : ''; + $sql .= $this->prepareRelatedSql($chainSearch, $model, 'OR', $fKey); + $sql .= ($csKey == count($searchFieldValues) - 1) ? ') ' : ''; + } + } else { + $sql .= $this->prepareRelatedSql($searchFieldValues, $model, 'AND', $fKey); + } + } + + $sql .= ' LIMIT 1)'; + } + + unset($modelObject); + } + + // create custom query sql + if (!empty($this->customSearchFields)) { + $modules = Modules::findFirstByName($classReflection->getShortName()); + + $sql .= ' INNER JOIN ' . $customClassname . ' ON ' . $customClassname . '.id = ('; + $sql .= 'SELECT ' . $customClassname . '.id FROM ' . $customClassname . ' WHERE ' . $customClassname . '.' . $classname . '_id = ' . $classname . '.id'; + + foreach ($this->customSearchFields as $fKey => $searchFieldValues) { + if (is_array(current($searchFieldValues))) { + foreach ($searchFieldValues as $csKey => $chainSearch) { + $sql .= !$csKey ? ' (' : ''; + $sql .= $this->prepareCustomSql($chainSearch, $modules, $customClassname, 'OR', $fKey); + $sql .= ($csKey == count($searchFieldValues) - 1) ? ') ' : ''; + } + } else { + $sql .= $this->prepareCustomSql($searchFieldValues, $modules, $customClassname, 'AND', $fKey); + } + } + + $sql .= ' LIMIT 1)'; + } + + $sql .= ' WHERE'; + + // create normal sql search + if (!empty($this->normalSearchFields)) { + foreach ($this->normalSearchFields as $fKey => $searchFieldValues) { + if (is_array(current($searchFieldValues))) { + foreach ($searchFieldValues as $csKey => $chainSearch) { + $sql .= !$csKey ? ' OR (' : ''; + $sql .= $this->prepareNormalSql($chainSearch, $classname, ($csKey ? 'OR' : ''), $fKey); + $sql .= ($csKey == count($searchFieldValues) - 1) ? ') ' : ''; + } + } else { + $sql .= $this->prepareNormalSql($searchFieldValues, $classname, 'AND', $fKey); + } + } + } + + // Replace initial `AND ` or `OR ` to avoid SQL errors. + $sql = str_replace( + ['WHERE AND', 'WHERE OR', 'WHERE ( OR'], + ['WHERE', 'WHERE', 'WHERE ('], + $sql + ); + + // Remove empty where from the end of the string. + $sql = preg_replace('# WHERE$#', '', $sql); + + //sql string + $countSql = 'SELECT COUNT(*) total FROM ' . $classname . $this->customTableJoins . $sql . $this->customConditions; + $resultsSql = "SELECT {$this->columns} {$this->customColumns} FROM {$classname} {$this->customTableJoins} {$sql} {$this->customConditions}"; + //bind params + $bindParams = array_combine($this->bindParamsKeys, $this->bindParamsValues); + + return [ + 'sql' => $resultsSql, + 'countSql' => $countSql, + 'bind' => $bindParams, + ]; + } + + /** + * Prepare the SQL for a normal search. + * + * @param array $searchCriteria + * @param string $classname + * @param string $andOr + * @param int $fKey + * + * @return string + */ + protected function prepareNormalSql(array $searchCriteria, string $classname, string $andOr, int $fKey): string + { + $sql = ''; + $textFields = $this->getTextFields($classname); + list($searchField, $operator, $searchValues) = $searchCriteria; + $operator = $this->operators[$operator]; + + if (trim($searchValues) !== '') { + if ($searchValues == '%%') { + $sql .= ' ' . $andOr . ' (' . $classname . '.' . $searchField . ' IS NULL'; + $sql .= ' OR ' . $classname . '.' . $searchField . ' = ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $classname . '.' . $searchField . ' = 0'; + } + + $sql .= ')'; + } elseif ($searchValues == '$$') { + $sql .= ' ' . $andOr . ' (' . $classname . '.' . $searchField . ' IS NOT NULL'; + $sql .= ' OR ' . $classname . '.' . $searchField . ' != ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $classname . '.' . $searchField . ' != 0'; + } + + $sql .= ')'; + } else { + if (strpos($searchValues, '|')) { + $searchValues = explode('|', $searchValues); + } else { + $searchValues = [$searchValues]; + } + + foreach ($searchValues as $vKey => $value) { + if ((in_array($searchField, $textFields) + && preg_match('#^%[^%]+%|%[^%]+|[^%]+%$#i', $value)) + || $value == '%%' + ) { + $operator = 'LIKE'; + } + + if ($value == 'null') { + $logicConector = !$vKey ? ' ' . $andOr . ' (' : ' OR '; + $sql .= $logicConector . $classname . '.' . $searchField . ' IS NULL'; + } else { + if (!$vKey) { + $sql .= ' ' . $andOr . ' (' . $classname . '.' . $searchField . ' ' . $operator . ' :f' . $searchField . $fKey . $vKey; + } else { + $sql .= ' OR ' . $classname . '.' . $searchField . ' ' . $operator . ' :f' . $searchField . $fKey . $vKey; + } + + $this->bindParamsKeys[] = 'f' . $searchField . $fKey . $vKey; + $this->bindParamsValues[] = $value; + } + } + + $sql .= ')'; + } + } + + return $sql; + } + + /** + * Prepare the SQL for a related search. + * + * @param array $searchCriteria + * @param string $classname + * @param string $andOr + * @param int $fKey + * + * @return string + */ + protected function prepareRelatedSql(array $searchCriteria, string $classname, string $andOr, int $fKey): string + { + $sql = ''; + $textFields = $this->getTextFields($classname); + list($searchField, $operator, $searchValues) = $searchCriteria; + $operator = $this->operators[$operator]; + + if (trim($searchValues) !== '') { + if ($searchValues == '%%') { + $sql .= ' ' . $andOr . ' (' . $classname . '.' . $searchField . ' IS NULL'; + $sql .= ' OR ' . $classname . '.' . $searchField . ' = ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $classname . '.' . $searchField . ' = 0'; + } + + $sql .= ')'; + } elseif ($searchValues == '$$') { + $sql .= ' ' . $andOr . ' (' . $classname . '.' . $searchField . ' IS NOT NULL'; + $sql .= ' OR ' . $classname . '.' . $searchField . ' != ""'; + + if ($this->model->$searchField === 0) { + $sql .= ' OR ' . $classname . '.' . $searchField . ' != 0'; + } + + $sql .= ')'; + } else { + if (strpos($searchValues, '|')) { + $searchValues = explode('|', $searchValues); + } else { + $searchValues = [$searchValues]; + } + + foreach ($searchValues as $vKey => $value) { + if (in_array($searchField, $textFields) + && preg_match('#^%[^%]+%|%[^%]+|[^%]+%$#i', $value) + ) { + $operator = 'LIKE'; + } + + if (!$vKey) { + $sql .= ' ' . $andOr . ' (' . $classname . '.' . $searchField . ' ' . $operator . ' :rf' . $searchField . $fKey . $vKey; + } else { + $sql .= ' OR ' . $classname . '.' . $searchField . ' ' . $operator . ' :rf' . $searchField . $fKey . $vKey; + } + + $this->bindParamsKeys[] = 'rf' . $searchField . $fKey . $vKey; + $this->bindParamsValues[] = $value; + } + + $sql .= ')'; + } + } + + return $sql; + } + + /** + * Prepare the SQL for a custom fields search. + * + * @param array $searchCriteria + * @param Model $modules + * @param string $classname + * @param string $andOr + * @param int $fKey + * + * @return string + */ + protected function prepareCustomSql(array $searchCriteria, Model $modules, string $classname, string $andOr, int $fKey): string + { + $sql = ''; + list($searchField, $operator, $searchValue) = $searchCriteria; + $operator = $this->operators[$operator]; + + if (trim($searchValue) !== '') { + $customFields = CustomFields::findFirst([ + 'modules_id = ?0 AND name = ?1', + 'bind' => [$modules->id, $searchField], + ]); + + $customFieldValue = $classname . '.value'; + if ($customFields->type->name == 'number') { + $customFieldValue = 'CAST(' . $customFieldValue . ' AS INT)'; + } + + $sql .= ' AND ' . $classname . '.custom_fields_id = :cfi' . $searchField; + + $this->bindParamsKeys[] = 'cfi' . $searchField; + $this->bindParamsValues[] = $customFields->id; + + if ($searchValue == '%%') { + $sql .= ' ' . $andOr . ' (' . $classname . '.value IS NULL OR ' . $classname . '.value = "")'; + } elseif ($searchValue == '$$') { + $sql .= ' ' . $andOr . ' (' . $classname . '.value IS NOT NULL OR ' . $classname . '.value != "")'; + } else { + if (strpos($searchValue, '|')) { + $searchValue = explode('|', $searchValue); + } else { + $searchValue = [$searchValue]; + } + + foreach ($searchValue as $vKey => $value) { + if (preg_match('#^%[^%]+%|%[^%]+|[^%]+%$#i', $value)) { + $operator = 'LIKE'; + } + + if (!$vKey) { + $sql .= ' ' . $andOr . ' (' . $customFieldValue . ' ' . $operator . ' :cfv' . $searchField . $fKey . $vKey; + } else { + $sql .= ' OR ' . $customFieldValue . ' ' . $operator . ' :cfv' . $searchField . $fKey . $vKey; + } + + $this->bindParamsKeys[] = 'cfv' . $searchField . $fKey . $vKey; + $this->bindParamsValues[] = $value; + } + + $sql .= ')'; + } + } + + return $sql; + } + + /** + * Preparse the parameters to be used in the search. + * + * @return void + */ + protected function prepareParams(array $unparsed): void + { + $this->relationSearchFields = array_key_exists('rparams', $unparsed) ? $this->parseRelationParameters($unparsed['rparams']) : $this->relationSearchFields; + $this->customSearchFields = array_key_exists('cparams', $unparsed) ? $this->parseSearchParameters($unparsed['cparams'])['mapped'] : []; + $this->normalSearchFields = array_key_exists('params', $unparsed) ? $this->parseSearchParameters($unparsed['params'])['mapped'] : []; + } + + /** + * Parse relationship query parameters. + * + * @param array $unparsed + * + * @return array + */ + protected function parseRelationParameters(array $unparsed): array + { + $parseRelationParameters = []; + $modelNamespace = Di::getDefault()->getConfig()->namespace->models; + + foreach ($unparsed as $model => $query) { + $modelName = str_replace(' ', '', ucwords(str_replace('_', ' ', $model))); + $modelName = $modelNamespace . '\\' . $modelName; + + if (!class_exists($modelName)) { + throw new Exception('Related model does not exist.'); + } + + $parseRelationParameters[$modelName] = $this->parseSearchParameters($query)['mapped']; + } + + return $parseRelationParameters; + } + + /** + * Parses out the search parameters from a request. + * Unparsed, they will look like this: + * (name:Benjamin Framklin,location:Philadelphia) + * Parsed: + * [ + * [ + * 'id_delete', + * ':', + * 0 + * ],[ + * 'id', + * '>', + * 0 + * ] + * ]. + * + * @param string $unparsed Unparsed search string + * @return array An array of fieldname=>value search parameters + */ + public function parseSearchParameters(string $unparsed): array + { + // $unparsed = urldecode($unparsed); + // Strip parens that come with the request string + $unparsed = trim($unparsed, '()'); + + // Now we have an array of "key:value" strings. + $splitFields = explode(',', $unparsed); + $sqlFilersOperators = implode('|', array_keys($this->operators)); + + $mapped = []; + $search = []; + + // Split the strings at their colon, set left to key, and right to value. + foreach ($splitFields as $key => $fieldChain) { + $hasChain = strpos($fieldChain, ';') !== false; + $fieldChain = explode(';', $fieldChain); + + foreach ($fieldChain as $field) { + $splitField = preg_split('#(' . $sqlFilersOperators . ')#', $field, -1, PREG_SPLIT_DELIM_CAPTURE); + + if (count($splitField) > 3) { + $splitField[2] = implode('', array_splice($splitField, 2)); + } + + if (!$hasChain) { + $mapped[$key] = $splitField; + } else { + $mapped[$key][] = $splitField; + } + + $search[$splitField[0]] = $splitField[2]; + } + } + + return [ + 'mapped' => $mapped, + 'search' => $search, + ]; + } + + /** + * Parses out the subquery parameters from a request. + * + * in = ::, not in = !:: + * + * Unparsed, they will look like this: + * internet_special(id::vehicles_id) + * Parsed: + * Array('action' => in, 'firstField' => id, 'secondField' => vehicles_id,'model' => MyDealer\Models\InternetSpecial) + * + * * + * @param string $unparsed Unparsed search string + * @return array An array of fieldname=>value search parameters + */ + protected function parseSubquery(string $unparsed): array + { + // Strip parens that come with the request string + $tableName = explode('(', $unparsed, 2); + //print_r($tableName);die(); + $tableName = strtolower($tableName[0]); + + $modelName = str_replace('_', ' ', $tableName); + $modelName = str_replace(' ', '', ucwords($modelName)); + + //Add the namespace to the model name + $model = $this->config['namespace']['models'] . '\\' . $modelName; + + $unparsed = str_replace($tableName, '', $unparsed); + $unparsed = trim($unparsed, '()'); + + // Now we have an array of "key:value" strings. + $splitFields = explode(',', $unparsed); + + if (strpos($splitFields[0], '!::') !== false) { + $action = 'not in'; + $fieldsToRelate = explode('!::', $splitFields[0]); + } elseif (strpos($splitFields[0], '::') !== false) { + $action = 'in'; + $fieldsToRelate = explode('::', $splitFields[0]); + } else { + throw new Exception('Error Processing Subquery', 1); + } + + $subquery = [ + 'action' => $action, + 'firstField' => $fieldsToRelate[0], + 'secondField' => $fieldsToRelate[1], + 'model' => $model, + ]; + + return $subquery; + } + + /** + * Prepare conditions to search in record. + * + * @param string $unparsed + * @return array + */ + protected function prepareSearch(array $unparsed, bool $isSearch = false, $hasSubquery = false): array + { + $statement = [ + 'conditions' => '1 = 1', + 'bind' => [], + ]; + + if ($isSearch) { + $mapped = $this->parseSearchParameters($unparsed['params']); + $conditions = '1 = 1'; + + $tmpMapped = $mapped; + + foreach ($tmpMapped as $key => $value) { + if (strpos($value, '~') !== false) { + unset($tmpMapped[$key]); + $betweenMap[$key] = explode('~', $value); + } + } + + $keys = array_keys($tmpMapped); + $values = array_values($tmpMapped); + + $di = Di::getDefault(); + + foreach ($keys as $key => $field) { + if ($di->get('config')->database->adapter == 'Postgresql') { + $conditions .= " AND CAST({$field} AS TEXT) LIKE ?{$key}"; + } else { + $conditions .= " AND {$field} LIKE ?{$key}"; + } + } + + if (isset($betweenMap)) { + foreach ($betweenMap as $key => $fields) { + $binds = count($values); + $conditions .= ' AND ' . $key . ' BETWEEN ?' . $binds . ' AND ?' . ($binds + 1); + $values = array_merge($values, $fields); + } + } + + if ($hasSubquery) { + $subquery = $this->parseSubquery($unparsed['subquery']); + $conditions .= ' AND ' . $subquery['firstField'] . ' ' . $subquery['action'] . ' (select ' . $subquery['secondField'] . ' FROM ' . $subquery['model'] . ')'; + } + + $statement = [ + 'conditions' => $conditions, + 'bind' => $values, + ]; + } + + return $statement; + } + + /** + * Parses out partial fields to return in the response. + * Unparsed: + * (id,name,location) + * Parsed: + * array('id', 'name', 'location'). + * + * @param string $unparsed Unparsed string of fields to return in partial response + * @return array Array of fields to return in partial response + */ + protected function parsePartialFields(string $unparsed): array + { + $fields = explode(',', trim($unparsed, '()')); + + // Avoid returning array with empty value + if (count($fields) == 1 && current($fields) == '') { + return []; + } + + return $fields; + } + + /** + * get the text field from this model database + * so we can do like search. + * + * @param string $table + * @return array + */ + protected function getTextFields($table): array + { + $columnsData = $this->model->getReadConnection()->describeColumns($table); + $textFields = []; + + foreach ($columnsData as $column) { + switch ($column->getType()) { + case \Phalcon\Db\Column::TYPE_VARCHAR: + case \Phalcon\Db\Column::TYPE_TEXT: + $textFields[] = $column->getName(); + break; + } + } + + return $textFields; + } + + /** + * Append any defined additional parameters. + * + * @return void + */ + public function appendAdditionalParams(): void + { + if (!empty($this->additionalSearchFields)) { + $this->normalSearchFields = array_merge_recursive($this->normalSearchFields, $this->additionalSearchFields); + } + + if (!empty($this->additionalCustomSearchFields)) { + $this->customSearchFields = array_merge_recursive($this->customSearchFields, $this->additionalCustomSearchFields); + } + + if (!empty($this->additionalRelationSearchFields)) { + $this->relationSearchFields = array_merge_recursive($this->relationSearchFields, $this->additionalRelationSearchFields); + } + } + + /** + * Append additional search parameters. + * + * @param array $params + * + * @return void + */ + public function appendParams(array $params): void + { + $this->additionalSearchFields = $params; + } + + /** + * Append additional search parameters. + * + * @param array $params + * + * @return void + */ + public function appendCustomParams(array $params): void + { + $this->additionalCustomSearchFields = $params; + } + + /** + * Append additional search parameters. + * + * @param array $params + * + * @return void + */ + public function appendRelationParams(array $params): void + { + $this->additionalRelationSearchFields = $params; + } + + /** + * Parse the requested columns to be returned. + * + * @param string $columns + * + * @return void + */ + protected function parseColumns(string $columns): void + { + // Split the columns string into individual columns + $columns = explode(',', $columns); + + foreach ($columns as &$column) { + $column = preg_replace('/[^a-zA-_Z]/', '', $column); + if (strpos($column, '.') === false) { + $column = "{$this->model->getSource()}.{$column}"; + } else { + $as = str_replace('.', '_', $column); + $column = "{$column} {$as}"; + } + } + + $this->columns = implode(', ', $columns); + } + + /** + * Get the limit. + * + * @return int + */ + public function getLimit(): int + { + return $this->limit; + } + + /** + * Get the page. + * + * @return int + */ + public function getPage(): int + { + return $this->page; + } + + /** + * Get the offset. + * + * @return int + */ + public function getOffset(): int + { + return $this->offset; + } + + /** + * Based on the given relaitonship , add the relation array to the Resultset. + * + * @param string $relationships + * @param [array|object] $results by reference to clean the object + * @return mixed + */ + public static function parseRelationShips(string $relationships, &$results): array + { + $relationships = explode(',', $relationships); + + $newResults = []; + + //if its a list + if ($results instanceof ResultsetInterface && count($results) >= 1) { + foreach ($results as $key => $result) { + //clean records conver to array + $newResults[$key] = $result->toFullArray(); + foreach ($relationships as $relationship) { + if ($results[$key]->$relationship) { + $callRelationship = 'get' . ucfirst($relationship); + $newResults[$key][$relationship] = $results[$key]->$callRelationship(); + } + } + } + } else { + //if its only 1 record + if ($results instanceof Model) { + $newResults = $results->toFullArray(); + foreach ($relationships as $relationship) { + if ($results->$relationship) { + $callRelationship = 'get' . ucfirst($relationship); + + $newResults[$relationship] = $results->$callRelationship(); + } + } + } + } + + unset($results); + return $newResults; + } + + /** + * Set CustomSort for the query. + * + * @param string $sort + * @return string + */ + public function setCustomSort(?string $sort): void + { + if (!is_null($sort)) { + // Get the model, column and sort order from the sent parameter. + list($modelColumn, $order) = explode('|', $sort); + // Check to see whether this is a related sorting by looking for a . + if (strpos($modelColumn, '.') !== false) { + // We are using a related sort. + // Get the namespace for the models from the configuration. + $modelNamespace = Di::getDefault()->getConfig()->namespace->models; + // Get the model name and the sort column from the sent parameter + list($model, $column) = explode('.', $modelColumn); + // Convert the model name into camel case. + $modelName = str_replace(' ', '', ucwords(str_replace('_', ' ', $model))); + // Create the model name with the appended namespace. + $modelName = $modelNamespace . '\\' . $modelName; + + // Make sure the model exists. + if (!class_exists($modelName)) { + throw new Exception('Related model does not exist.'); + } + + // Instance the model so we have access to the getSource() function. + $modelObject = new $modelName(); + // Instance meta data memory to access the primary keys for the table. + $metaData = new MetaDataMemory(); + + // Get the first matching primary key. + // @TODO This will hurt on compound primary keys. + $primaryKey = $metaData->getPrimaryKeyAttributes($modelObject)[0]; + // We need the table to exist in the query in order for the related sort to work. + // Therefore we add it to comply with this by comparing the primary key to not being NULL. + $this->relationSearchFields[$modelName][] = [ + $primaryKey, ':', '$$', + ]; + + $this->sort = " ORDER BY {$modelObject->getSource()}.{$column} {$order}"; + unset($modelObject); + } else { + $this->sort = " ORDER BY {$modelColumn} {$order}"; + } + } + } +} diff --git a/src/Middleware/Response.php b/src/Middleware/Response.php new file mode 100644 index 0000000..783d825 --- /dev/null +++ b/src/Middleware/Response.php @@ -0,0 +1,35 @@ +getService('response'); + $response->send(); + + return true; + } +} \ No newline at end of file diff --git a/src/Request/Baka.php b/src/Request/Baka.php new file mode 100644 index 0000000..590809c --- /dev/null +++ b/src/Request/Baka.php @@ -0,0 +1,34 @@ +getPost() ?: $this->getJsonRawBody(true); + + return $data ?: []; + } + + /** + * Get the data from a POST request. + * + * @return void + */ + public function getPutData() + { + $data = $this->getPut() ?: $this->getJsonRawBody(true); + + return $data ?: []; + } +} diff --git a/src/Request/Swoole.php b/src/Request/Swoole.php new file mode 100644 index 0000000..ac143d8 --- /dev/null +++ b/src/Request/Swoole.php @@ -0,0 +1,1345 @@ + +// +---------------------------------------------------------------------- + +namespace Baka\Http\Request; + +use Phalcon\DiInterface; +use Phalcon\Events\Manager; +use Phalcon\FilterInterface; +use Phalcon\Http\RequestInterface; +use Phalcon\Di\InjectionAwareInterface; +use Phalcon\Http\Request\File; +use Phalcon\Text; +use swoole_http_request; +use Exception; +use Phalcon\Di\FactoryDefault; + +/** + * Class SwooleRequest. + * + * To use Swoole Server with Phalcon we need to overwrite the Phalcon Request Object to use swoole Respnose object + * Since swoole is our server he is the one who get all our _GET , _FILES, _POST , _PUT request and we need to parse that info + * to make our phalcon project work + * + * @package Gewaer\Http + * + * @property \Phalcon\Di $di + */ +class Swoole implements RequestInterface, InjectionAwareInterface +{ + protected $_dependencyInjector; + + protected $_httpMethodParameterOverride = false; + + protected $_filter; + + protected $_putCache; + + protected $_strictHostCheck = false; + + protected $_files; + + protected $_rawBody; + + protected $headers; + + protected $server; + + protected $get; + + protected $post; + + protected $cookies; + + protected $files; + + protected $swooleRequest; + + /** + * Init the object with Swoole reqeust. + * + * @param swoole_http_request $request + * @return void + */ + public function init(swoole_http_request $request): void + { + $this->swooleRequest = $request; + $this->headers = []; + $this->server = []; + + $this->get = isset($request->get) ? $request->get : []; + $this->post = isset($request->post) ? $request->post : []; + $this->cookies = isset($request->cookie) ? $request->cookie : []; + $this->files = isset($request->files) ? $request->files : []; + $this->_rawBody = $request->rawContent(); + + //iterate header + $this->setGlobalHeaders($request->header); + $this->setGlobalServers($request->server); + + //iterate server + + /** @var Cookies $cookies */ + //$cookies = FactoryDefault::getDefault()->getCookies(); + // $cookies->setSwooleCookies($this->cookies); + } + + /** + * Set global headers. + * + * @param array $headers + * @return void + */ + private function setGlobalHeaders(array $headers): void + { + foreach ($headers as $key => $val) { + $key = strtoupper(str_replace(['-'], '_', $key)); + $this->headers[$key] = $val; + $this->server[$key] = $val; + } + } + + /** + * Set global Servers. + * + * @param array $servers + * @return void + */ + private function setGlobalServers(array $servers): void + { + foreach ($servers as $key => $val) { + $key = strtoupper(str_replace(['-'], '_', $key)); + $this->server[$key] = $val; + } + } + + /** + * Set Di. + * + * @param DiInterface $dependencyInjector + * @return void + */ + public function setDI(DiInterface $dependencyInjector) + { + $this->_dependencyInjector = $dependencyInjector; + } + + /** + * Get Di. + * + * @return void + */ + public function getDI() + { + return $this->_dependencyInjector; + } + + /** + * Access to REQUEST. + * + * @param string $name + * @param string $filters + * @param string $defaultValue + * @param boolean $notAllowEmpty + * @param boolean $noRecursive + * @return array|string + */ + public function get($name = null, $filters = null, $defaultValue = null, $notAllowEmpty = false, $noRecursive = false) + { + $source = array_merge($this->get, $this->post); + return $this->getHelper($source, $name, $filters, $defaultValue, $notAllowEmpty, $noRecursive); + } + + /** + * Acces to Post. + * + * @param string $name + * @param string $filters + * @param string $defaultValue + * @param boolean $notAllowEmpty + * @param boolean $noRecursive + * @return array|string + */ + public function getPost($name = null, $filters = null, $defaultValue = null, $notAllowEmpty = false, $noRecursive = false) + { + $source = $this->post; + return $this->getHelper($source, $name, $filters, $defaultValue, $notAllowEmpty, $noRecursive); + } + + /** + * Access to GET. + * + * @param string $name + * @param string $filters + * @param string $defaultValue + * @param boolean $notAllowEmpty + * @param boolean $noRecursive + * @return array|string + */ + public function getQuery($name = null, $filters = null, $defaultValue = null, $notAllowEmpty = false, $noRecursive = false) + { + $source = $this->get; + return $this->getHelper($source, $name, $filters, $defaultValue, $notAllowEmpty, $noRecursive); + } + + /** + * Get _SERVER. + * + * @param string $name + * @return string|null + */ + public function getServer($name) + { + $name = strtoupper(str_replace(['-'], '_', $name)); + if (isset($this->server[$name])) { + return $this->server[$name]; + } + + return null; + } + + /** + * Get _PUT. + * + * @param string $name + * @param string $filters + * @param string $defaultValue + * @param boolean $notAllowEmpty + * @param boolean $noRecursive + * @return array|string + */ + public function getPut($name = null, $filters = null, $defaultValue = null, $notAllowEmpty = false, $noRecursive = false) + { + $put = $this->_putCache; + + if (empty($put)) { + json_decode($this->getRawBody()); + //return (bool ) (json_last_error() == JSON_ERROR_NONE); + //confirm is a true json reponse + if ((json_last_error() == JSON_ERROR_NONE)) { + parse_str($this->getRawBody(), $put); + } else { + $put = $this->getJsonRawBody(true); + } + $this->_putCache = $put; + } + + return $this->getHelper($put, $name, $filters, $defaultValue, $notAllowEmpty, $noRecursive); + } + + /** + * Has. + * + * @param string $name + * @return boolean + */ + public function has($name) + { + $source = array_merge($this->get, $this->post); + return isset($source[$name]); + } + + /** + * Has Post. + * + * @param string $name + * @return boolean + */ + public function hasPost($name) + { + return isset($this->post[$name]); + } + + /** + * Has Put. + * + * @param string $name + * @return boolean + */ + public function hasPut($name) + { + $put = $this->getPut(); + + return isset($put[$name]); + } + + /** + * Has GET. + * + * @param string $name + * @return boolean + */ + public function hasQuery($name) + { + return isset($this->get[$name]); + } + + /** + * Has SERVER. + * + * @param string $name + * @return boolean + */ + public function hasServer($name) + { + $name = strtoupper(str_replace(['-'], '_', $name)); + + return isset($this->server[$name]); + } + + /** + * Has HEADER. + * + * @param string $name + * @return boolean + */ + public function hasHeader($header) + { + if ($this->hasServer($header)) { + return true; + } + if ($this->hasServer('HTTP_' . $header)) { + return true; + } + return false; + } + + /** + * Get Header. + * + * @param string $name + * @return string|void + */ + public function getHeader($header) + { + $header = $this->getServer($header); + if (isset($header)) { + return $header; + } + + $header = $this->getServer('HTTP_' . $header); + if (isset($header)) { + return $header; + } + + return ''; + } + + /** + * Get Schema. + * + * @return string + */ + public function getScheme() + { + $https = $this->getServer('HTTPS'); + if ($https && $https != 'off') { + return 'https'; + } + + return 'http'; + } + + /** + * Is ajax. + * + * @return boolean + */ + public function isAjax() + { + return $this->getServer('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest'; + } + + /** + * is Soap. + * + * @return boolean + */ + public function isSoap() + { + if ($this->hasServer('HTTP_SOAPACTION')) { + return true; + } + + $contentType = $this->getContentType(); + if (!empty($contentType)) { + return (bool) strpos($contentType, 'application/soap+xml') !== false; + } + + return false; + } + + /** + * is Soap. + * + * @return boolean + */ + public function isSoapRequested() + { + return $this->isSoap(); + } + + /** + * is HTTPS. + * + * @return boolean + */ + public function isSecure() + { + return $this->getScheme() === 'https'; + } + + /** + * is HTTPS. + * + * @return boolean + */ + public function isSecureRequest() + { + return $this->isSecure(); + } + + /** + * get RAW. + * + * @return string + */ + public function getRawBody() + { + return $this->_rawBody; + } + + /** + * Get json. + * + * @param boolean $associative + * @return void|string + */ + public function getJsonRawBody($associative = false) + { + $rawBody = $this->getRawBody(); + if (!is_string($rawBody)) { + return false; + } + + return json_decode($rawBody, $associative); + } + + /** + * Get servers addres. + * + * @return string + */ + public function getServerAddress() + { + $serverAddr = $this->getServer('SERVER_ADDR'); + if ($serverAddr) { + return $serverAddr; + } + + return gethostbyname('localhost'); + } + + /** + * Get server name. + * + * @return string + */ + public function getServerName() + { + $serverName = $this->getServer('SERVER_NAME'); + if ($serverName) { + return $serverName; + } + + return 'localhost'; + } + + /** + * Get https hosts. + * + * @return string + */ + public function getHttpHost() + { + $strict = $this->_strictHostCheck; + + /** + * Get the server name from $_SERVER["HTTP_HOST"]. + */ + $host = $this->getServer('HTTP_HOST'); + if (!$host) { + /** + * Get the server name from $_SERVER["SERVER_NAME"]. + */ + $host = $this->getServer('SERVER_NAME'); + if (!$host) { + /** + * Get the server address from $_SERVER["SERVER_ADDR"]. + */ + $host = $this->getServer('SERVER_ADDR'); + } + } + + if ($host && $strict) { + /** + * Cleanup. Force lowercase as per RFC 952/2181. + */ + $host = strtolower(trim($host)); + if (strpos($host, ':') !== false) { + $host = preg_replace('/:[[:digit:]]+$/', '', $host); + } + + /** + * Host may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner), + * the digits '0' through '9', and the hyphen ('-') as per RFC 952/2181. + */ + if ('' !== preg_replace("/[a-z0-9-]+\.?/", '', $host)) { + throw new \UnexpectedValueException('Invalid host ' . $host); + } + } + + return (string) $host; + } + + /** + * Sets if the `Request::getHttpHost` method must be use strict validation of host name or not. + */ + public function setStrictHostCheck($flag = true) + { + $this->_strictHostCheck = $flag; + + return $this; + } + + /** + * Checks if the `Request::getHttpHost` method will be use strict validation of host name or not. + */ + public function isStrictHostCheck() + { + return $this->_strictHostCheck; + } + + /** + * Get port. + * + * @return int + */ + public function getPort() + { + /** + * Get the server name from $_SERVER["HTTP_HOST"]. + */ + $host = $this->getServer('HTTP_HOST'); + if ($host) { + if (strpos($host, ':') !== false) { + $pos = strrpos($host, ':'); + + if (false !== $pos) { + return (int)substr($host, $pos + 1); + } + + return 'https' === $this->getScheme() ? 443 : 80; + } + } + return (int) $this->getServer('SERVER_PORT'); + } + + /** + * Gets HTTP URI which request has been made. + */ + public function getURI() + { + $requestURI = $this->getServer('request_uri'); //$this->getServer('REQUEST_URI') == $this->getQuery('_url') ? $this->getServer('REQUEST_URI') : $this->getQuery('_url'); + if ($requestURI) { + return $requestURI; + } + + return ''; + } + + /** + * Get client ip. + * + * @param boolean $trustForwardedHeader + * @return string|boolean + */ + public function getClientAddress($trustForwardedHeader = true) + { + $address = null; + + /** + * Proxies uses this IP. + */ + if ($trustForwardedHeader) { + $address = $this->getServer('X_FORWARDED_FOR'); + if ($address === null) { + $address = $this->getServer('X_REAL_IP'); + } + } + + if ($address === null) { + $address = $this->getServer('REMOTE_ADDR'); + } + + if (is_string($address)) { + if (strpos($address, ',') !== false) { + /** + * The client address has multiples parts, only return the first part. + */ + return explode(',', $address)[0]; + } + return $address; + } + + return false; + } + + /** + * Get method. + * + * @return string + */ + public function getMethod() + { + $returnMethod = $this->getServer('REQUEST_METHOD'); + if (!isset($returnMethod)) { + return 'GET'; + } + + $returnMethod = strtoupper($returnMethod); + if ($returnMethod === 'POST') { + $overridedMethod = $this->getHeader('X-HTTP-METHOD-OVERRIDE'); + if (!empty($overridedMethod)) { + $returnMethod = strtoupper($overridedMethod); + } elseif ($this->_httpMethodParameterOverride) { + if ($spoofedMethod = $this->get('_method')) { + $returnMethod = strtoupper($spoofedMethod); + } + } + } + + if (!$this->isValidHttpMethod($returnMethod)) { + return 'GET'; + } + + return $returnMethod; + } + + /** + * Get user agent. + * + * @return string|void + */ + public function getUserAgent() + { + $userAgent = $this->getServer('HTTP_USER_AGENT'); + if ($userAgent) { + return $userAgent; + } + return ''; + } + + /** + * Is method. + * + * @param string $methods + * @param boolean $strict + * @return boolean + */ + public function isMethod($methods, $strict = false) + { + $httpMethod = $this->getMethod(); + + if (is_string($methods)) { + if ($strict && !$this->isValidHttpMethod($methods)) { + throw new Exception('Invalid HTTP method: ' . $methods); + } + return $methods == $httpMethod; + } + + if (is_array($methods)) { + foreach ($methods as $method) { + if ($this->isMethod($method, $strict)) { + return true; + } + } + + return false; + } + + if ($strict) { + throw new Exception('Invalid HTTP method: non-string'); + } + + return false; + } + + /** + * Is post. + * + * @return boolean + */ + public function isPost() + { + return $this->getMethod() === 'POST'; + } + + /** + * Is GET. + * + * @return boolean + */ + public function isGet() + { + return $this->getMethod() === 'GET'; + } + + /** + * Is Put. + * + * @return boolean + */ + public function isPut() + { + return $this->getMethod() === 'PUT'; + } + + /** + * Is patch. + * + * @return boolean + */ + public function isPatch() + { + return $this->getMethod() === 'PATCH'; + } + + /** + * Is head. + * + * @return boolean + */ + public function isHead() + { + return $this->getMethod() === 'HEAD'; + } + + /** + * Is dealete. + * + * @return boolean + */ + public function isDelete() + { + return $this->getMethod() === 'DELETE'; + } + + /** + * Is Options. + * + * @return boolean + */ + public function isOptions() + { + return $this->getMethod() === 'OPTIONS'; + } + + /** + * Is Purge. + * + * @return boolean + */ + public function isPurge() + { + return $this->getMethod() === 'PURGE'; + } + + /** + * Is trace. + * + * @return boolean + */ + public function isTrace() + { + return $this->getMethod() === 'TRACE'; + } + + /** + * Is connect. + * + * @return boolean + */ + public function isConnect() + { + return $this->getMethod() === 'CONNECT'; + } + + /** + * Has uploaded files? + * + * @param boolean $onlySuccessful + * @return string + */ + public function hasFiles($onlySuccessful = false) + { + $numberFiles = 0; + + $files = $this->files; + + if (empty($files)) { + return $numberFiles; + } + + foreach ($files as $file) { + $error = $file['error']; + if ($error) { + if (!is_array($error)) { + if (!$error || !$onlySuccessful) { + $numberFiles++; + } + } else { + $numberFiles += $this->hasFileHelper($error, $onlySuccessful); + } + } + } + + return $numberFiles; + } + + /** + * Recursively counts file in an array of files. + */ + protected function hasFileHelper($data, $onlySuccessful) + { + $numberFiles = 0; + + if (!is_array($data)) { + return 1; + } + + foreach ($data as $value) { + if (!is_array($value)) { + if (!$value || !$onlySuccessful) { + $numberFiles++; + } + } else { + $numberFiles += $this->hasFileHelper($value, $onlySuccessful); + } + } + + return $numberFiles; + } + + /** + * Get the uploaded files. + * + * @param boolean $onlySuccessful + * @return array + */ + public function getUploadedFiles($onlySuccessful = false) + { + $files = []; + + $superFiles = $this->files; + + if (count($superFiles) > 0) { + foreach ($superFiles as $prefix => $input) { + if (is_array(!$input['name'])) { + $smoothInput = $this->smoothFiles( + $input['name'], + $input['type'], + $input['tmp_name'], + $input['size'], + $input['error'], + $prefix + ); + + foreach ($smoothInput as $file) { + if ($onlySuccessful === false || $file['error'] == UPLOAD_ERR_OK) { + $dataFile = [ + 'name' => $file['name'], + 'type' => $file['type'], + 'tmp_name' => $file['tmp_name'], + 'size' => $file['size'], + 'error' => $file['error'] + ]; + + $files[] = new File($dataFile, $file['key']); + } + } + } else { + if ($onlySuccessful === false || $input['error'] == UPLOAD_ERR_OK) { + $files[] = new File($input, $prefix); + } + } + } + } + + return $files; + } + + /** + * Get the files. + * + * @param string $key + * @return string|void + */ + public function getFile($key) + { + if (!isset($this->_files)) { + $this->_files = []; + $files = $this->getUploadedFiles(); + foreach ($files as $file) { + $this->_files[$file->getKey()] = $file; + } + } + + if (!isset($this->_files[$key])) { + return null; + } + + return $this->_files[$key]; + } + + /** + * Smooth out $_FILES to have plain array with all files uploaded. + */ + protected function smoothFiles($names, $types, $tmp_names, $sizes, $errors, $prefix) + { + $files = []; + + foreach ($names as $idx => $name) { + $p = $prefix . '.' . $idx; + + if (is_string($name)) { + $files[] = [ + 'name' => $name, + 'type' => $types[$idx], + 'tmp_name' => $tmp_names[$idx], + 'size' => $sizes[$idx], + 'error' => $errors[$idx], + 'key' => $p + ]; + } + + if (is_array($name)) { + $parentFiles = $this->smoothFiles( + $names[$idx], + $types[$idx], + $tmp_names[$idx], + $sizes[$idx], + $errors[$idx], + $p + ); + + foreach ($parentFiles as $file) { + $files[] = $file; + } + } + } + + return $files; + } + + /** + * Get the servers. + * + * @return array + */ + public function getServers() + { + return $this->server; + } + + /** + * Get the headers. + * + * @return array + */ + public function getHeaders() + { + $headers = []; + $contentHeaders = ['CONTENT_TYPE' => true, 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true]; + + $servers = $this->getServers(); + foreach ($servers as $name => $value) { + if (Text::startsWith($name, 'HTTP_')) { + $name = ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))); + $name = str_replace(' ', '-', $name); + $headers[$name] = $value; + } + + $name = strtoupper($name); + if (isset($contentHeaders[$name])) { + $name = ucwords(strtolower(str_replace('_', ' ', $name))); + $name = str_replace(' ', '-', $name); + $headers[$name] = $value; + } + } + + $authHeaders = $this->resolveAuthorizationHeaders(); + + // Protect for future (child classes) changes + if (is_array($authHeaders)) { + $headers = array_merge($headers, $authHeaders); + } + + return $headers; + } + + /** + * Get the httpd reference. + * + * @return string|void + */ + public function getHTTPReferer() + { + $httpReferer = $this->getServer('HTTP_REFERER'); + if ($httpReferer) { + return $httpReferer; + } + + return ''; + } + + /** + * Process a request header and return the one with best quality. + * + * @return string + */ + protected function _getBestQuality($qualityParts, $name) + { + $i = 0; + $quality = 0.0; + $selectedName = ''; + + foreach ($qualityParts as $accept) { + if ($i == 0) { + $quality = (double)$accept['quality']; + $selectedName = $accept[$name]; + } else { + $acceptQuality = (double)$accept['quality']; + if ($acceptQuality > $quality) { + $quality = $acceptQuality; + $selectedName = $accept[$name]; + } + } + $i++; + } + + return $selectedName; + } + + /** + * Get the content. + * + * @return array + */ + public function getAcceptableContent() + { + return $this->_getQualityHeader('HTTP_ACCEPT', 'accept'); + } + + /** + * Get the content. + * + * @return string + */ + public function getBestAccept() + { + return $this->_getBestQuality($this->getAcceptableContent(), 'accept'); + } + + /** + * Get the content. + * + * @return array + */ + public function getClientCharsets() + { + return $this->_getQualityHeader('HTTP_ACCEPT_CHARSET', 'charset'); + } + + /** + * Get the content. + * + * @return string + */ + public function getBestCharset() + { + return $this->_getBestQuality($this->getClientCharsets(), 'charset'); + } + + /** + * Get the content. + * + * @return array + */ + public function getLanguages() + { + return $this->_getQualityHeader('HTTP_ACCEPT_LANGUAGE', 'language'); + } + + /** + * Get the content. + * + * @return string + */ + public function getBestLanguage() + { + return $this->_getBestQuality($this->getLanguages(), 'language'); + } + + /** + * Get the basic httpd auth. + * + * @return array|void + */ + public function getBasicAuth() + { + if ($this->hasServer('PHP_AUTH_USER') && $this->hasServer('PHP_AUTH_PW')) { + return [ + 'username' => $this->getServer('PHP_AUTH_USER'), + 'password' => $this->getServer('PHP_AUTH_PW') + ]; + } + + return null; + } + + /** + * Get the server digest. + * + * @return array + */ + public function getDigestAuth() + { + $auth = []; + if ($this->hasServer('PHP_AUTH_DIGEST')) { + $digest = $this->getServer('PHP_AUTH_DIGEST'); + $matches = []; + if (!preg_match_all("#(\\w+)=(['\"]?)([^'\" ,]+)\\2#", $digest, $matches, 2)) { + return $auth; + } + if (is_array($matches)) { + foreach ($matches as $match) { + $auth[$match[1]] = $match[3]; + } + } + } + + return $auth; + } + + /** + * Checks if a method is a valid HTTP method. + */ + public function isValidHttpMethod($method) + { + switch (strtoupper($method)) { + case 'GET': + case 'POST': + case 'PUT': + case 'DELETE': + case 'HEAD': + case 'OPTIONS': + case 'PATCH': + case 'PURGE': // Squid and Varnish support + case 'TRACE': + case 'CONNECT': + return true; + } + + return false; + } + + /** + * Helper to get data from superglobals, applying filters if needed. + * If no parameters are given the superglobal is returned. + */ + protected function getHelper($source, $name = null, $filters = null, $defaultValue = null, $notAllowEmpty = false, $noRecursive = false) + { + if ($name === null) { + return $source; + } + + if (!isset($source[$name])) { + return $defaultValue; + } + + $value = $source[$name]; + + if ($filters !== null) { + $filter = $this->_filter; + if (!$filter instanceof FilterInterface) { + $dependencyInjector = $this->_dependencyInjector; + if (!$dependencyInjector instanceof DiInterface) { + throw new Exception("A dependency injection object is required to access the 'filter' service"); + } + + $filter = $dependencyInjector->getShared('filter'); + $this->_filter = $filter; + } + + $value = $filter->sanitize($value, $filters, $noRecursive); + } + + if (empty($value) && $notAllowEmpty === true) { + return $defaultValue; + } + + return $value; + } + + /** + * Gets content type which request has been made. + */ + public function getContentType() + { + $contentType = $this->getHeader('CONTENT_TYPE'); + if ($contentType) { + return $contentType; + } + + return null; + } + + /** + * Process a request header and return an array of values with their qualities. + * + * @return array + */ + protected function _getQualityHeader($serverIndex, $name) + { + $returnedParts = []; + $parts = preg_split('/,\\s*/', $this->getServer($serverIndex), -1, PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + $headerParts = []; + $hParts = preg_split("/\s*;\s*/", trim($part), -1, PREG_SPLIT_NO_EMPTY); + foreach ($hParts as $headerPart) { + if (strpos($headerPart, '=') !== false) { + $split = explode('=', $headerPart, 2); + if ($split[0] === 'q') { + $headerParts['quality'] = (double)$split[1]; + } else { + $headerParts[$split[0]] = $split[1]; + } + } else { + $headerParts[$name] = $headerPart; + $headerParts['quality'] = 1.0; + } + } + + $returnedParts[] = $headerParts; + } + + return $returnedParts; + } + + /** + * Resolve authorization headers. + */ + protected function resolveAuthorizationHeaders() + { + $headers = []; + $hasEventsManager = false; + $eventsManager = null; + + $dependencyInjector = $this->getDI(); + if ($dependencyInjector instanceof DiInterface) { + $hasEventsManager = (bool)$dependencyInjector->has('eventsManager'); + if ($hasEventsManager) { + $eventsManager = $dependencyInjector->getShared('eventsManager'); + } + } + + if ($hasEventsManager && $eventsManager instanceof Manager) { + $resolved = $eventsManager->fire( + 'request:beforeAuthorizationResolve', + $this, + ['server' => $this->getServers()] + ); + + if (is_array($resolved)) { + $headers = array_merge($headers, $resolved); + } + } + + $this->resolveAuthHeaderPhp($headers); + $this->resolveAuthHeaderPhpDigest($headers); + + if ($hasEventsManager && $eventsManager instanceof Manager) { + $resolved = $eventsManager->fire( + 'request:afterAuthorizationResolve', + $this, + ['headers' => $headers, 'server' => $this->getServers()] + ); + + if (is_array($resolved)) { + $headers = array_merge($headers, $resolved); + } + } + + return $headers; + } + + /** + * Resolve the PHP_AUTH_USER. + * + * @param array $headers + * @return void + */ + protected function resolveAuthHeaderPhp(array &$headers): void + { + $authHeader = false; + + if ($this->hasServer('PHP_AUTH_USER') && $this->hasServer('PHP_AUTH_PW')) { + $headers['Php-Auth-User'] = $this->getServer('PHP_AUTH_USER'); + $headers['Php-Auth-Pw'] = $this->getServer('PHP_AUTH_PW'); + } else { + if ($this->hasServer('HTTP_AUTHORIZATION')) { + $authHeader = $this->getServer('HTTP_AUTHORIZATION'); + } elseif ($this->hasServer('REDIRECT_HTTP_AUTHORIZATION')) { + $authHeader = $this->getServer('REDIRECT_HTTP_AUTHORIZATION'); + } + + if ($authHeader) { + if (stripos($authHeader, 'basic ') === 0) { + $exploded = explode(':', base64_decode(substr($authHeader, 6)), 2); + if (count($exploded) == 2) { + $headers['Php-Auth-User'] = $exploded[0]; + $headers['Php-Auth-Pw'] = $exploded[1]; + } + } elseif (stripos($authHeader, 'digest ') === 0 && !$this->hasServer('PHP_AUTH_DIGEST')) { + $headers['Php-Auth-Digest'] = $authHeader; + } elseif (stripos($authHeader, 'bearer ') === 0) { + $headers['Authorization'] = $authHeader; + } + } + } + } + + /** + * Reseolve PHP auth digest. + * + * @param array $headers + * @return void + */ + protected function resolveAuthHeaderPhpDigest(array &$headers): void + { + if (!isset($headers['Authorization'])) { + if (isset($headers['Php-Auth-User'])) { + $headers['Authorization'] = 'Basic ' . base64_encode($headers['Php-Auth-User'] . ':' . $headers['Php-Auth-Pw']); + } elseif (isset($headers['Php-Auth-Digest'])) { + $headers['Authorization'] = $headers['Php-Auth-Digest']; + } + } + } +} diff --git a/src/Response/Swoole.php b/src/Response/Swoole.php new file mode 100644 index 0000000..4d537c0 --- /dev/null +++ b/src/Response/Swoole.php @@ -0,0 +1,86 @@ + +// +---------------------------------------------------------------------- + +namespace Baka\Http\Response; + +use Phalcon\Http\Cookie; +use Phalcon\Http\Response as PhResponse; +use swoole_http_response; +use Exception; + +class Swoole extends Response +{ + protected $response; + + /** + * Set the swoole response object. + * + * @param swoole_http_response $response + * @return void + */ + public function init(swoole_http_response $response): void + { + $this->response = $response; + $this->_sent = false; + $this->_content = null; + $this->setStatusCode(200); + } + + /** + * Send the response. + * + * @return PhResponse + */ + public function send(): PhResponse + { + if ($this->_sent) { + throw new Exception('Response was already sent'); + } + + $this->_sent = true; + // get phalcon headers + $headers = $this->getHeaders(); + + foreach ($headers->toArray() as $key => $val) { + //if the key has spaces this breaks postman, so we remove this headers + //example: HTTP/1.1 200 OK || HTTP/1.1 401 Unauthorized + if (!preg_match('/\s/', $key)) { + $this->response->header($key, $val); + } + } + + /** @var Cookies $cookies */ + $cookies = $this->getCookies(); + if ($cookies) { + /** @var Cookie $cookie */ + foreach ($cookies->getCookies() as $cookie) { + $this->response->cookie( + $cookie->getName(), + $cookie->getValue(), + $cookie->getExpiration(), + $cookie->getPath(), + $cookie->getDomain(), + $cookie->getSecure(), + $cookie->getHttpOnly() + ); + } + } + + //set swoole response + $this->response->status($this->getStatusCode()); + $this->response->end($this->_content); + + //reest di + $this->_sent = false; + $this->getDi()->get('db')->close(); + $this->getDi()->reset(); + + return $this; + } +} diff --git a/src/Router/Collection.php b/src/Router/Collection.php new file mode 100644 index 0000000..7895082 --- /dev/null +++ b/src/Router/Collection.php @@ -0,0 +1,239 @@ +setHandler("MyDealer\Controllers\IndexController", true); + * $index->setPrefix("/"); + * $index->get("", "index"); + * $application->mount($index); + * + * We provide a clean API to emulate Phalcon MVC Routers + * + * $router = new RouterCollection($application); + * $router->setPrefix('/v2'); + * $router->get('/', [ + * 'MyDealer\Controllers\IndexController', + * 'index', + * ]); + * + * $router->post('/add', [ + * 'MyDealer\Controllers\IndexController', + * 'index', + * ]); + * + * $router->mount(); + */ +class Collection +{ + private $application; + private $prefix = null; + private $collections = []; + private static $jwt = []; + private static $hasJwtOptionsSetup = false; + private static $middleware = []; + + /** + * Constructor , we pass the micro app. + * + * @param Micro $application + */ + public function __construct(Micro $application) + { + $this->application = $application; + } + + /** + * If the router is user a prefix. + * + * @param string $prefix + */ + public function setPrefix(string $prefix): void + { + $this->prefix = $prefix; + } + + /** + * Mount the collection to the micro app router. + * + * @return void + */ + public function mount(): void + { + if (count($this->collections) > 0) { + foreach ($this->collections as $collection) { + $micro = new MicroCollection(); + // Set the main handler. ie. a controller instance + $micro->setHandler($collection['className'], true); + // Set a common prefix for all routes + + if ($this->prefix) { + $micro->setPrefix($this->prefix); + } + + // Use the method 'index' in PostsController + $micro->{$collection['method']}($collection['pattern'], $collection['function']); + + $this->application->mount($micro); + } + } + + return; + } + + /** + * Add the call function to the collection array. + * + * @param string $method + * @param string $pattern + * @param string $className + * @param string $function + * @return void + */ + private function call(string $method, string $pattern, string $className, string $function, array $options = []): void + { + if (empty($className) || empty($function)) { + throw new Exception('Missing params, we need 2 parameters'); + } + + $route = [ + 'method' => $method, + 'pattern' => $pattern, + 'className' => $className, + 'function' => $function, + ]; + $this->collections[] = $route; + + if (array_key_exists('options', $options)) { + $this->setOptions($route, $options); + } + + return; + } + + /** + * Set routers options JWT. + * + * @todo add Middleware that the router will call + * @param array $route + * @param array $options + * @return void + */ + private function setOptions(array $route, array $options): void + { + if (array_key_exists('jwt', $options['options'])) { + //only add if we want to ignore this url + if (!$options['options']['jwt']) { + self::$hasJwtOptionsSetup = true; + //we group them by method and hash the pattern to make it a faster lookup + self::$jwt[strtoupper($route['method'])][md5($this->prefix . $route['pattern'])] = $this->prefix . $route['pattern']; + } + } + } + + /** + * Get the ignore JWT url. + * + * @return array + */ + public static function getJwtIgnoreRoutes(): array + { + $ignoreUrl = []; + if (self::$hasJwtOptionsSetup) { + $ignoreUrl = self::$jwt; + } + + return $ignoreUrl; + } + + /** + * Insted of using magic we define each method function. + * + * @param string $pattern + * @param array $param + * @return void + */ + public function get(string $pattern, array $param): void + { + $this->call('get', $pattern, $param[0], $param[1], $param); + } + + /** + * Insted of using magic we define each method function. + * + * @param string $pattern + * @param array $param + * @return void + */ + public function put(string $pattern, array $param) : void + { + $this->call('put', $pattern, $param[0], $param[1], $param); + } + + /** + * Insted of using magic we define each method function. + * + * @param string $pattern + * @param array $param + * @return void + */ + public function post(string $pattern, array $param) : void + { + $this->call('post', $pattern, $param[0], $param[1], $param); + } + + /** + * Insted of using magic we define each method function. + * + * @param string $pattern + * @param array $param + * @return void + */ + public function delete(string $pattern, array $param) : void + { + $this->call('delete', $pattern, $param[0], $param[1], $param); + } + + /** + * Insted of using magic we define each method function. + * + * @param string $pattern + * @param array $param + * @return void + */ + public function patch(string $pattern, array $param) : void + { + $this->call('patch', $pattern, $param[0], $param[1], $param); + } + + /** + * Insted of using magic we define each method function. + * + * @param string $pattern + * @param array $param + * @return void + */ + public function options(string $pattern, array $param) : void + { + $this->call('options', $pattern, $param[0], $param[1], $param); + } + + /** + * Instead of using magic we define each method function. + * + * @param string $pattern + * @param array $param + */ + public function head(string $pattern, array $param) + { + return $this->call('head', $pattern, $param[0], $param[1]); + } +} diff --git a/storage/db/migrations/20190408003104_ls.php b/storage/db/migrations/20190408003104_ls.php new file mode 100644 index 0000000..9c74fff --- /dev/null +++ b/storage/db/migrations/20190408003104_ls.php @@ -0,0 +1,138 @@ +execute("ALTER DATABASE CHARACTER SET 'utf8mb4';"); + $this->execute("ALTER DATABASE COLLATE='utf8mb4_unicode_ci';"); + $this->table('leads', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8', + 'collation' => 'utf8_general_ci', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_REGULAR, + 'precision' => '10', + 'identity' => 'enable', + ]) + ->addColumn('users_id', 'integer', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_REGULAR, + 'precision' => '10', + 'after' => 'id', + ]) + ->addColumn('companies_id', 'integer', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_REGULAR, + 'precision' => '10', + 'after' => 'users_id', + ]) + ->addColumn('firstname', 'string', [ + 'null' => true, + 'default' => 'NULL', + 'limit' => 45, + 'collation' => 'utf8_general_ci', + 'encoding' => 'utf8', + 'after' => 'companies_id', + ]) + ->addColumn('lastname', 'string', [ + 'null' => true, + 'default' => 'NULL', + 'limit' => 45, + 'collation' => 'utf8_general_ci', + 'encoding' => 'utf8', + 'after' => 'firstname', + ]) + ->addColumn('email', 'string', [ + 'null' => true, + 'default' => 'NULL', + 'limit' => 45, + 'collation' => 'utf8_general_ci', + 'encoding' => 'utf8', + 'after' => 'lastname', + ]) + ->addColumn('phone', 'string', [ + 'null' => true, + 'default' => 'NULL', + 'limit' => 45, + 'collation' => 'utf8_general_ci', + 'encoding' => 'utf8', + 'after' => 'email', + ]) + ->addColumn('leads_owner_id', 'integer', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_REGULAR, + 'precision' => '10', + 'after' => 'phone', + ]) + ->addColumn('leads_status_id', 'integer', [ + 'null' => false, + 'default' => '1', + 'limit' => MysqlAdapter::INT_REGULAR, + 'precision' => '10', + 'after' => 'leads_owner_id', + ]) + ->addColumn('created_at', 'datetime', [ + 'null' => false, + 'after' => 'leads_status_id', + ]) + ->addColumn('updated_at', 'datetime', [ + 'null' => true, + // 'default' => 'NULL', + 'after' => 'created_at', + ]) + ->addColumn('is_deleted', 'integer', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_TINY, + 'precision' => '3', + 'after' => 'updated_at', + ]) + ->addColumn('is_duplicated', 'integer', [ + 'null' => false, + 'default' => '0', + 'limit' => MysqlAdapter::INT_REGULAR, + 'precision' => '10', + 'after' => 'is_deleted', + ]) + ->addColumn('is_active', 'integer', [ + 'null' => true, + 'default' => '1', + 'limit' => MysqlAdapter::INT_REGULAR, + 'precision' => '10', + 'after' => 'is_duplicated', + ]) + ->addIndex(['users_id'], [ + 'name' => 'users_id', + 'unique' => false, + ]) + ->addIndex(['companies_id'], [ + 'name' => 'companies_id', + 'unique' => false, + ]) + ->addIndex(['leads_owner_id'], [ + 'name' => 'leads_owner_id', + 'unique' => false, + ]) + ->addIndex(['leads_status_id'], [ + 'name' => 'leads_status_id', + 'unique' => false, + ]) + ->addIndex(['email'], [ + 'name' => 'email', + 'unique' => false, + ]) + ->addIndex(['id', 'companies_id', 'is_deleted'], [ + 'name' => 'id', + 'unique' => false, + ]) + ->create(); + } +} diff --git a/storage/db/migrations/schema.php b/storage/db/migrations/schema.php new file mode 100644 index 0000000..9b5635a --- /dev/null +++ b/storage/db/migrations/schema.php @@ -0,0 +1,680 @@ + + array ( + 'default_character_set_name' => 'utf8mb4', + 'default_collation_name' => 'utf8mb4_unicode_ci', + ), + 'tables' => + array ( + 'leads' => + array ( + 'table' => + array ( + 'table_name' => 'leads', + 'engine' => 'InnoDB', + 'table_comment' => ' ', + 'table_collation' => 'utf8_general_ci', + 'character_set_name' => 'utf8', + 'row_format' => 'Dynamic', + ), + 'columns' => + array ( + 'id' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'COLUMN_DEFAULT' => NULL, + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'int', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '10', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'int(11)', + 'COLUMN_KEY' => 'PRI', + 'EXTRA' => 'auto_increment', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'users_id' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'users_id', + 'ORDINAL_POSITION' => '2', + 'COLUMN_DEFAULT' => NULL, + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'int', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '10', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'int(11)', + 'COLUMN_KEY' => 'MUL', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'companies_id' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'companies_id', + 'ORDINAL_POSITION' => '3', + 'COLUMN_DEFAULT' => NULL, + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'int', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '10', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'int(11)', + 'COLUMN_KEY' => 'MUL', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'firstname' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'firstname', + 'ORDINAL_POSITION' => '4', + 'COLUMN_DEFAULT' => 'NULL', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'varchar', + 'CHARACTER_MAXIMUM_LENGTH' => '45', + 'CHARACTER_OCTET_LENGTH' => '135', + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => 'utf8', + 'COLLATION_NAME' => 'utf8_general_ci', + 'COLUMN_TYPE' => 'varchar(45)', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'lastname' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'lastname', + 'ORDINAL_POSITION' => '5', + 'COLUMN_DEFAULT' => 'NULL', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'varchar', + 'CHARACTER_MAXIMUM_LENGTH' => '45', + 'CHARACTER_OCTET_LENGTH' => '135', + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => 'utf8', + 'COLLATION_NAME' => 'utf8_general_ci', + 'COLUMN_TYPE' => 'varchar(45)', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'email' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'email', + 'ORDINAL_POSITION' => '6', + 'COLUMN_DEFAULT' => 'NULL', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'varchar', + 'CHARACTER_MAXIMUM_LENGTH' => '45', + 'CHARACTER_OCTET_LENGTH' => '135', + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => 'utf8', + 'COLLATION_NAME' => 'utf8_general_ci', + 'COLUMN_TYPE' => 'varchar(45)', + 'COLUMN_KEY' => 'MUL', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'phone' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'phone', + 'ORDINAL_POSITION' => '7', + 'COLUMN_DEFAULT' => 'NULL', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'varchar', + 'CHARACTER_MAXIMUM_LENGTH' => '45', + 'CHARACTER_OCTET_LENGTH' => '135', + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => 'utf8', + 'COLLATION_NAME' => 'utf8_general_ci', + 'COLUMN_TYPE' => 'varchar(45)', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'leads_owner_id' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'leads_owner_id', + 'ORDINAL_POSITION' => '8', + 'COLUMN_DEFAULT' => NULL, + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'int', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '10', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'int(11)', + 'COLUMN_KEY' => 'MUL', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'leads_status_id' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'leads_status_id', + 'ORDINAL_POSITION' => '9', + 'COLUMN_DEFAULT' => '1', + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'int', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '10', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'int(11)', + 'COLUMN_KEY' => 'MUL', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'created_at' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'created_at', + 'ORDINAL_POSITION' => '10', + 'COLUMN_DEFAULT' => NULL, + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'datetime', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => '0', + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'datetime', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'updated_at' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'updated_at', + 'ORDINAL_POSITION' => '11', + 'COLUMN_DEFAULT' => 'NULL', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'datetime', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => '0', + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'datetime', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'is_deleted' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'is_deleted', + 'ORDINAL_POSITION' => '12', + 'COLUMN_DEFAULT' => NULL, + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'tinyint', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '3', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'tinyint(4)', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'is_duplicated' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'is_duplicated', + 'ORDINAL_POSITION' => '13', + 'COLUMN_DEFAULT' => '0', + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'int', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '10', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'int(11)', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'is_active' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'leads', + 'COLUMN_NAME' => 'is_active', + 'ORDINAL_POSITION' => '14', + 'COLUMN_DEFAULT' => '1', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'int', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '10', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'int(1)', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + ), + 'indexes' => + array ( + 'PRIMARY' => + array ( + 1 => + array ( + 'Table' => 'leads', + 'Non_unique' => '0', + 'Key_name' => 'PRIMARY', + 'Seq_in_index' => '1', + 'Column_name' => 'id', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + ), + 'users_id' => + array ( + 1 => + array ( + 'Table' => 'leads', + 'Non_unique' => '1', + 'Key_name' => 'users_id', + 'Seq_in_index' => '1', + 'Column_name' => 'users_id', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + ), + 'companies_id' => + array ( + 1 => + array ( + 'Table' => 'leads', + 'Non_unique' => '1', + 'Key_name' => 'companies_id', + 'Seq_in_index' => '1', + 'Column_name' => 'companies_id', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + ), + 'leads_owner_id' => + array ( + 1 => + array ( + 'Table' => 'leads', + 'Non_unique' => '1', + 'Key_name' => 'leads_owner_id', + 'Seq_in_index' => '1', + 'Column_name' => 'leads_owner_id', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + ), + 'leads_status_id' => + array ( + 1 => + array ( + 'Table' => 'leads', + 'Non_unique' => '1', + 'Key_name' => 'leads_status_id', + 'Seq_in_index' => '1', + 'Column_name' => 'leads_status_id', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + ), + 'email' => + array ( + 1 => + array ( + 'Table' => 'leads', + 'Non_unique' => '1', + 'Key_name' => 'email', + 'Seq_in_index' => '1', + 'Column_name' => 'email', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => 'YES', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + ), + 'id' => + array ( + 1 => + array ( + 'Table' => 'leads', + 'Non_unique' => '1', + 'Key_name' => 'id', + 'Seq_in_index' => '1', + 'Column_name' => 'id', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + 2 => + array ( + 'Table' => 'leads', + 'Non_unique' => '1', + 'Key_name' => 'id', + 'Seq_in_index' => '2', + 'Column_name' => 'companies_id', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + 3 => + array ( + 'Table' => 'leads', + 'Non_unique' => '1', + 'Key_name' => 'id', + 'Seq_in_index' => '3', + 'Column_name' => 'is_deleted', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + ), + ), + 'foreign_keys' => NULL, + ), + 'ut_migrations' => + array ( + 'table' => + array ( + 'table_name' => 'ut_migrations', + 'engine' => 'InnoDB', + 'table_comment' => '', + 'table_collation' => 'utf8_general_ci', + 'character_set_name' => 'utf8', + 'row_format' => 'Dynamic', + ), + 'columns' => + array ( + 'version' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'ut_migrations', + 'COLUMN_NAME' => 'version', + 'ORDINAL_POSITION' => '1', + 'COLUMN_DEFAULT' => NULL, + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'bigint', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '19', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'bigint(20)', + 'COLUMN_KEY' => 'PRI', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'migration_name' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'ut_migrations', + 'COLUMN_NAME' => 'migration_name', + 'ORDINAL_POSITION' => '2', + 'COLUMN_DEFAULT' => 'NULL', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'varchar', + 'CHARACTER_MAXIMUM_LENGTH' => '100', + 'CHARACTER_OCTET_LENGTH' => '300', + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => 'utf8', + 'COLLATION_NAME' => 'utf8_general_ci', + 'COLUMN_TYPE' => 'varchar(100)', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'start_time' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'ut_migrations', + 'COLUMN_NAME' => 'start_time', + 'ORDINAL_POSITION' => '3', + 'COLUMN_DEFAULT' => 'NULL', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'timestamp', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => '0', + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'timestamp', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'end_time' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'ut_migrations', + 'COLUMN_NAME' => 'end_time', + 'ORDINAL_POSITION' => '4', + 'COLUMN_DEFAULT' => 'NULL', + 'IS_NULLABLE' => 'YES', + 'DATA_TYPE' => 'timestamp', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => NULL, + 'NUMERIC_SCALE' => NULL, + 'DATETIME_PRECISION' => '0', + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'timestamp', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + 'breakpoint' => + array ( + 'TABLE_CATALOG' => 'def', + 'TABLE_NAME' => 'ut_migrations', + 'COLUMN_NAME' => 'breakpoint', + 'ORDINAL_POSITION' => '5', + 'COLUMN_DEFAULT' => '0', + 'IS_NULLABLE' => 'NO', + 'DATA_TYPE' => 'tinyint', + 'CHARACTER_MAXIMUM_LENGTH' => NULL, + 'CHARACTER_OCTET_LENGTH' => NULL, + 'NUMERIC_PRECISION' => '3', + 'NUMERIC_SCALE' => '0', + 'DATETIME_PRECISION' => NULL, + 'CHARACTER_SET_NAME' => NULL, + 'COLLATION_NAME' => NULL, + 'COLUMN_TYPE' => 'tinyint(1)', + 'COLUMN_KEY' => '', + 'EXTRA' => '', + 'PRIVILEGES' => 'select,insert,update,references', + 'COLUMN_COMMENT' => '', + 'IS_GENERATED' => 'NEVER', + 'GENERATION_EXPRESSION' => NULL, + ), + ), + 'indexes' => + array ( + 'PRIMARY' => + array ( + 1 => + array ( + 'Table' => 'ut_migrations', + 'Non_unique' => '0', + 'Key_name' => 'PRIMARY', + 'Seq_in_index' => '1', + 'Column_name' => 'version', + 'Collation' => 'A', + 'Sub_part' => NULL, + 'Packed' => NULL, + 'Null' => '', + 'Index_type' => 'BTREE', + 'Comment' => '', + 'Index_comment' => '', + ), + ), + ), + 'foreign_keys' => NULL, + ), + ), +); \ No newline at end of file diff --git a/storage/db/seeds/LeadSeeder.php b/storage/db/seeds/LeadSeeder.php new file mode 100644 index 0000000..cba7be9 --- /dev/null +++ b/storage/db/seeds/LeadSeeder.php @@ -0,0 +1,61 @@ + 1, + 'companies_id' => 1, + 'firstname' => 'Max', + 'lastname' => 'Castro', + 'email' => 'something@about.com', + 'phone' => '5555555555', + 'leads_owner_id' => 1, + 'leads_status_id' => 1, + 'created_at' => date('Y-m-d H:m:s'), + 'updated_at' => date('Y-m-d H:m:s'), + 'is_deleted' => 0, + 'is_duplicated' => 0, + 'is_active' => 1, + ], + [ + 'users_id' => 1, + 'companies_id' => 2, + 'firstname' => 'Leo', + 'lastname' => 'Castro', + 'email' => 'anotheremail@about.com', + 'phone' => '5555555555', + 'leads_owner_id' => 1, + 'leads_status_id' => 2, + 'created_at' => date('Y-m-d H:m:s'), + 'updated_at' => date('Y-m-d H:m:s'), + 'is_deleted' => 0, + 'is_duplicated' => 0, + 'is_active' => 1, + ], + [ + 'users_id' => 1, + 'companies_id' => 3, + 'firstname' => 'Campo', + 'lastname' => 'Castro', + 'email' => 'somethingelse@about.com', + 'phone' => '5555555555', + 'leads_owner_id' => 1, + 'leads_status_id' => 3, + 'created_at' => date('Y-m-d H:m:s'), + 'updated_at' => date('Y-m-d H:m:s'), + 'is_deleted' => 1, + 'is_duplicated' => 0, + 'is_active' => 1, + ], + ]; + + $posts = $this->table('leads'); + $posts->insert($data) + ->save(); + } +} diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php new file mode 100755 index 0000000..b1d5f4e --- /dev/null +++ b/tests/_bootstrap.php @@ -0,0 +1,30 @@ +registerNamespaces( + [ + 'Baka\Http' => ROOT_DIR . 'src', + 'Test\Model' => ROOT_DIR . 'tests\_support\Model', + 'Test\Indices' => ROOT_DIR . 'tests\_support\Indices', + 'Baka\Elasticsearch' => ROOT_DIR . '..\phalcon-elasticsearch\src\\', + 'Baka\Database' => ROOT_DIR . '..\database\src\\' + ] +); + +$loader->register(); + +$dotenv = new Dotenv\Dotenv(__DIR__ . '/../'); +$dotenv->load(); diff --git a/tests/_data/dump.sql b/tests/_data/dump.sql new file mode 100755 index 0000000..4bc742c --- /dev/null +++ b/tests/_data/dump.sql @@ -0,0 +1 @@ +/* Replace this file with actual dump of your database */ \ No newline at end of file diff --git a/tests/_output/.gitignore b/tests/_output/.gitignore new file mode 100755 index 0000000..c96a04f --- /dev/null +++ b/tests/_output/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/tests/_support/AcceptanceTester.php b/tests/_support/AcceptanceTester.php new file mode 100755 index 0000000..4c7dcbb --- /dev/null +++ b/tests/_support/AcceptanceTester.php @@ -0,0 +1,26 @@ +id = $this->id; + $this->setId($this->id); + $object->users_id = 1; + $object->companies_id = 2; + $object->firstname = 'Max'; + $object->lastname = 'Castro'; + $object->email = 'wazadfadf@somethinggood.com'; + $object->is_deleted = 0; + + $company = [ + 'id' => 1, + 'name' => 'mc', + 'url' => 'http://mctekk.com', + 'branch_id' => 1, + 'branch' => [ + 'id' => 2, + 'name' => 'DN', + ] + ]; + + $object->company = $company; + return $object; + } + + /** + * Define the structure of thies index. + * + * @return array + */ + public function structure(): array + { + return [ + 'id' => $this->integer, + 'users_id' => $this->integer, + 'companies_id' => $this->integer, + 'firstname' => $this->text, + 'lastname' => $this->text, + 'email' => $this->text, + 'is_deleted' => $this->integer, + 'company' => [ + 'id' => $this->integer, + 'name' => $this->text, + 'url' => $this->text, + 'branch_id' => $this->integer, + 'branch' => [ + 'id' => $this->integer, + 'name' => $this->text, + ] + ] + ]; + } +} diff --git a/tests/_support/Model/Leads.php b/tests/_support/Model/Leads.php new file mode 100644 index 0000000..cea1360 --- /dev/null +++ b/tests/_support/Model/Leads.php @@ -0,0 +1,16 @@ +getScenario()->runStep(new \Codeception\Step\Action('setHeader', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Authenticates user for HTTP_AUTH + * + * @param $username + * @param $password + * @see \Codeception\Module\PhpBrowser::amHttpAuthenticated() + */ + public function amHttpAuthenticated($username, $password) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amHttpAuthenticated', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Open web page at the given absolute URL and sets its hostname as the base host. + * + * ``` php + * amOnUrl('http://codeception.com'); + * $I->amOnPage('/quickstart'); // moves to http://codeception.com/quickstart + * ?> + * ``` + * @see \Codeception\Module\PhpBrowser::amOnUrl() + */ + public function amOnUrl($url) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amOnUrl', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Changes the subdomain for the 'url' configuration parameter. + * Does not open a page; use `amOnPage` for that. + * + * ``` php + * amOnSubdomain('user'); + * $I->amOnPage('/'); + * // moves to http://user.mysite.com/ + * ?> + * ``` + * + * @param $subdomain + * + * @return mixed + * @see \Codeception\Module\PhpBrowser::amOnSubdomain() + */ + public function amOnSubdomain($subdomain) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amOnSubdomain', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Low-level API method. + * If Codeception commands are not enough, use [Guzzle HTTP Client](http://guzzlephp.org/) methods directly + * + * Example: + * + * ``` php + * executeInGuzzle(function (\GuzzleHttp\Client $client) { + * $client->get('/get', ['query' => ['foo' => 'bar']]); + * }); + * ?> + * ``` + * + * It is not recommended to use this command on a regular basis. + * If Codeception lacks important Guzzle Client methods, implement them and submit patches. + * + * @param callable $function + * @see \Codeception\Module\PhpBrowser::executeInGuzzle() + */ + public function executeInGuzzle($function) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('executeInGuzzle', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sets the HTTP header to the passed value - which is used on + * subsequent HTTP requests through PhpBrowser. + * + * Example: + * ```php + * haveHttpHeader('X-Requested-With', 'Codeception'); + * $I->amOnPage('test-headers.php'); + * ?> + * ``` + * + * To use special chars in Header Key use HTML Character Entities: + * Example: + * Header with underscore - 'Client_Id' + * should be represented as - 'Client_Id' or 'Client_Id' + * + * ```php + * haveHttpHeader('Client_Id', 'Codeception'); + * ?> + * ``` + * + * @param string $name the name of the request header + * @param string $value the value to set it to for subsequent + * requests + * @see \Codeception\Lib\InnerBrowser::haveHttpHeader() + */ + public function haveHttpHeader($name, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('haveHttpHeader', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Deletes the header with the passed name. Subsequent requests + * will not have the deleted header in its request. + * + * Example: + * ```php + * haveHttpHeader('X-Requested-With', 'Codeception'); + * $I->amOnPage('test-headers.php'); + * // ... + * $I->deleteHeader('X-Requested-With'); + * $I->amOnPage('some-other-page.php'); + * ?> + * ``` + * + * @param string $name the name of the header to delete. + * @see \Codeception\Lib\InnerBrowser::deleteHeader() + */ + public function deleteHeader($name) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('deleteHeader', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Opens the page for the given relative URI. + * + * ``` php + * amOnPage('/'); + * // opens /register page + * $I->amOnPage('/register'); + * ``` + * + * @param string $page + * @see \Codeception\Lib\InnerBrowser::amOnPage() + */ + public function amOnPage($page) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amOnPage', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Perform a click on a link or a button, given by a locator. + * If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string. + * For buttons, the "value" attribute, "name" attribute, and inner text are searched. + * For links, the link text is searched. + * For images, the "alt" attribute and inner text of any parent links are searched. + * + * The second parameter is a context (CSS or XPath locator) to narrow the search. + * + * Note that if the locator matches a button of type `submit`, the form will be submitted. + * + * ``` php + * click('Logout'); + * // button of form + * $I->click('Submit'); + * // CSS button + * $I->click('#form input[type=submit]'); + * // XPath + * $I->click('//form/*[@type="submit"]'); + * // link in context + * $I->click('Logout', '#nav'); + * // using strict locator + * $I->click(['link' => 'Login']); + * ?> + * ``` + * + * @param $link + * @param $context + * @see \Codeception\Lib\InnerBrowser::click() + */ + public function click($link, $context = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('click', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string (case insensitive). + * + * You can specify a specific HTML element (via CSS or XPath) as the second + * parameter to only search within that element. + * + * ``` php + * see('Logout'); // I can suppose user is logged in + * $I->see('Sign Up', 'h1'); // I can suppose it's a signup page + * $I->see('Sign Up', '//body/h1'); // with XPath + * $I->see('Sign Up', ['css' => 'body h1']); // with strict CSS locator + * ``` + * + * Note that the search is done after stripping all HTML tags from the body, + * so `$I->see('strong')` will return true for strings like: + * + * - `

I am Stronger than thou

` + * - `` + * + * But will *not* be true for strings like: + * + * - `Home` + * - `
Home` + * - `` + * + * For checking the raw source code, use `seeInSource()`. + * + * @param string $text + * @param array|string $selector optional + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::see() + */ + public function canSee($text, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('see', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string (case insensitive). + * + * You can specify a specific HTML element (via CSS or XPath) as the second + * parameter to only search within that element. + * + * ``` php + * see('Logout'); // I can suppose user is logged in + * $I->see('Sign Up', 'h1'); // I can suppose it's a signup page + * $I->see('Sign Up', '//body/h1'); // with XPath + * $I->see('Sign Up', ['css' => 'body h1']); // with strict CSS locator + * ``` + * + * Note that the search is done after stripping all HTML tags from the body, + * so `$I->see('strong')` will return true for strings like: + * + * - `

I am Stronger than thou

` + * - `` + * + * But will *not* be true for strings like: + * + * - `Home` + * - `
Home` + * - `` + * + * For checking the raw source code, use `seeInSource()`. + * + * @param string $text + * @param array|string $selector optional + * @see \Codeception\Lib\InnerBrowser::see() + */ + public function see($text, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('see', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page doesn't contain the text specified (case insensitive). + * Give a locator as the second parameter to match a specific region. + * + * ```php + * dontSee('Login'); // I can suppose user is already logged in + * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page + * $I->dontSee('Sign Up','//body/h1'); // with XPath + * $I->dontSee('Sign Up', ['css' => 'body h1']); // with strict CSS locator + * ``` + * + * Note that the search is done after stripping all HTML tags from the body, + * so `$I->dontSee('strong')` will fail on strings like: + * + * - `

I am Stronger than thou

` + * - `` + * + * But will ignore strings like: + * + * - `Home` + * - `
Home` + * - `` + * + * For checking the raw source code, use `seeInSource()`. + * + * @param string $text + * @param array|string $selector optional + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSee() + */ + public function cantSee($text, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSee', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page doesn't contain the text specified (case insensitive). + * Give a locator as the second parameter to match a specific region. + * + * ```php + * dontSee('Login'); // I can suppose user is already logged in + * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page + * $I->dontSee('Sign Up','//body/h1'); // with XPath + * $I->dontSee('Sign Up', ['css' => 'body h1']); // with strict CSS locator + * ``` + * + * Note that the search is done after stripping all HTML tags from the body, + * so `$I->dontSee('strong')` will fail on strings like: + * + * - `

I am Stronger than thou

` + * - `` + * + * But will ignore strings like: + * + * - `Home` + * - `
Home` + * - `` + * + * For checking the raw source code, use `seeInSource()`. + * + * @param string $text + * @param array|string $selector optional + * @see \Codeception\Lib\InnerBrowser::dontSee() + */ + public function dontSee($text, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSee', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string in its + * raw source code. + * + * ``` php + * seeInSource('

Green eggs & ham

'); + * ``` + * + * @param $raw + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeInSource() + */ + public function canSeeInSource($raw) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInSource', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string in its + * raw source code. + * + * ``` php + * seeInSource('

Green eggs & ham

'); + * ``` + * + * @param $raw + * @see \Codeception\Lib\InnerBrowser::seeInSource() + */ + public function seeInSource($raw) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInSource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string in its + * raw source code. + * + * ```php + * dontSeeInSource('

Green eggs & ham

'); + * ``` + * + * @param $raw + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeInSource() + */ + public function cantSeeInSource($raw) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInSource', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string in its + * raw source code. + * + * ```php + * dontSeeInSource('

Green eggs & ham

'); + * ``` + * + * @param $raw + * @see \Codeception\Lib\InnerBrowser::dontSeeInSource() + */ + public function dontSeeInSource($raw) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeInSource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there's a link with the specified text. + * Give a full URL as the second parameter to match links with that exact URL. + * + * ``` php + * seeLink('Logout'); // matches Logout + * $I->seeLink('Logout','/logout'); // matches Logout + * ?> + * ``` + * + * @param string $text + * @param string $url optional + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeLink() + */ + public function canSeeLink($text, $url = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeLink', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there's a link with the specified text. + * Give a full URL as the second parameter to match links with that exact URL. + * + * ``` php + * seeLink('Logout'); // matches Logout + * $I->seeLink('Logout','/logout'); // matches Logout + * ?> + * ``` + * + * @param string $text + * @param string $url optional + * @see \Codeception\Lib\InnerBrowser::seeLink() + */ + public function seeLink($text, $url = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeLink', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page doesn't contain a link with the given string. + * If the second parameter is given, only links with a matching "href" attribute will be checked. + * + * ``` php + * dontSeeLink('Logout'); // I suppose user is not logged in + * $I->dontSeeLink('Checkout now', '/store/cart.php'); + * ?> + * ``` + * + * @param string $text + * @param string $url optional + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeLink() + */ + public function cantSeeLink($text, $url = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeLink', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page doesn't contain a link with the given string. + * If the second parameter is given, only links with a matching "href" attribute will be checked. + * + * ``` php + * dontSeeLink('Logout'); // I suppose user is not logged in + * $I->dontSeeLink('Checkout now', '/store/cart.php'); + * ?> + * ``` + * + * @param string $text + * @param string $url optional + * @see \Codeception\Lib\InnerBrowser::dontSeeLink() + */ + public function dontSeeLink($text, $url = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeLink', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that current URI contains the given string. + * + * ``` php + * seeInCurrentUrl('home'); + * // to match: /users/1 + * $I->seeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param string $uri + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeInCurrentUrl() + */ + public function canSeeInCurrentUrl($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInCurrentUrl', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that current URI contains the given string. + * + * ``` php + * seeInCurrentUrl('home'); + * // to match: /users/1 + * $I->seeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Lib\InnerBrowser::seeInCurrentUrl() + */ + public function seeInCurrentUrl($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInCurrentUrl', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URI doesn't contain the given string. + * + * ``` php + * dontSeeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param string $uri + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeInCurrentUrl() + */ + public function cantSeeInCurrentUrl($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInCurrentUrl', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URI doesn't contain the given string. + * + * ``` php + * dontSeeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Lib\InnerBrowser::dontSeeInCurrentUrl() + */ + public function dontSeeInCurrentUrl($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeInCurrentUrl', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL is equal to the given string. + * Unlike `seeInCurrentUrl`, this only matches the full URL. + * + * ``` php + * seeCurrentUrlEquals('/'); + * ?> + * ``` + * + * @param string $uri + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeCurrentUrlEquals() + */ + public function canSeeCurrentUrlEquals($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeCurrentUrlEquals', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL is equal to the given string. + * Unlike `seeInCurrentUrl`, this only matches the full URL. + * + * ``` php + * seeCurrentUrlEquals('/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Lib\InnerBrowser::seeCurrentUrlEquals() + */ + public function seeCurrentUrlEquals($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeCurrentUrlEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL doesn't equal the given string. + * Unlike `dontSeeInCurrentUrl`, this only matches the full URL. + * + * ``` php + * dontSeeCurrentUrlEquals('/'); + * ?> + * ``` + * + * @param string $uri + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeCurrentUrlEquals() + */ + public function cantSeeCurrentUrlEquals($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeCurrentUrlEquals', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL doesn't equal the given string. + * Unlike `dontSeeInCurrentUrl`, this only matches the full URL. + * + * ``` php + * dontSeeCurrentUrlEquals('/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Lib\InnerBrowser::dontSeeCurrentUrlEquals() + */ + public function dontSeeCurrentUrlEquals($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeCurrentUrlEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL matches the given regular expression. + * + * ``` php + * seeCurrentUrlMatches('~^/users/(\d+)~'); + * ?> + * ``` + * + * @param string $uri + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeCurrentUrlMatches() + */ + public function canSeeCurrentUrlMatches($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeCurrentUrlMatches', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL matches the given regular expression. + * + * ``` php + * seeCurrentUrlMatches('~^/users/(\d+)~'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Lib\InnerBrowser::seeCurrentUrlMatches() + */ + public function seeCurrentUrlMatches($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeCurrentUrlMatches', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that current url doesn't match the given regular expression. + * + * ``` php + * dontSeeCurrentUrlMatches('~^/users/(\d+)~'); + * ?> + * ``` + * + * @param string $uri + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeCurrentUrlMatches() + */ + public function cantSeeCurrentUrlMatches($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeCurrentUrlMatches', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that current url doesn't match the given regular expression. + * + * ``` php + * dontSeeCurrentUrlMatches('~^/users/(\d+)~'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Lib\InnerBrowser::dontSeeCurrentUrlMatches() + */ + public function dontSeeCurrentUrlMatches($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeCurrentUrlMatches', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Executes the given regular expression against the current URI and returns the first capturing group. + * If no parameters are provided, the full URI is returned. + * + * ``` php + * grabFromCurrentUrl('~^/user/(\d+)/~'); + * $uri = $I->grabFromCurrentUrl(); + * ?> + * ``` + * + * @param string $uri optional + * + * @return mixed + * @see \Codeception\Lib\InnerBrowser::grabFromCurrentUrl() + */ + public function grabFromCurrentUrl($uri = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabFromCurrentUrl', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the specified checkbox is checked. + * + * ``` php + * seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. + * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); + * ?> + * ``` + * + * @param $checkbox + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeCheckboxIsChecked() + */ + public function canSeeCheckboxIsChecked($checkbox) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeCheckboxIsChecked', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the specified checkbox is checked. + * + * ``` php + * seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. + * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); + * ?> + * ``` + * + * @param $checkbox + * @see \Codeception\Lib\InnerBrowser::seeCheckboxIsChecked() + */ + public function seeCheckboxIsChecked($checkbox) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeCheckboxIsChecked', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Check that the specified checkbox is unchecked. + * + * ``` php + * dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. + * ?> + * ``` + * + * @param $checkbox + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeCheckboxIsChecked() + */ + public function cantSeeCheckboxIsChecked($checkbox) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeCheckboxIsChecked', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Check that the specified checkbox is unchecked. + * + * ``` php + * dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. + * ?> + * ``` + * + * @param $checkbox + * @see \Codeception\Lib\InnerBrowser::dontSeeCheckboxIsChecked() + */ + public function dontSeeCheckboxIsChecked($checkbox) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeCheckboxIsChecked', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given input field or textarea *equals* (i.e. not just contains) the given value. + * Fields are matched by label text, the "name" attribute, CSS, or XPath. + * + * ``` php + * seeInField('Body','Type your comment here'); + * $I->seeInField('form textarea[name=body]','Type your comment here'); + * $I->seeInField('form input[type=hidden]','hidden_value'); + * $I->seeInField('#searchform input','Search'); + * $I->seeInField('//form/*[@name=search]','Search'); + * $I->seeInField(['name' => 'search'], 'Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeInField() + */ + public function canSeeInField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInField', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given input field or textarea *equals* (i.e. not just contains) the given value. + * Fields are matched by label text, the "name" attribute, CSS, or XPath. + * + * ``` php + * seeInField('Body','Type your comment here'); + * $I->seeInField('form textarea[name=body]','Type your comment here'); + * $I->seeInField('form input[type=hidden]','hidden_value'); + * $I->seeInField('#searchform input','Search'); + * $I->seeInField('//form/*[@name=search]','Search'); + * $I->seeInField(['name' => 'search'], 'Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see \Codeception\Lib\InnerBrowser::seeInField() + */ + public function seeInField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInField', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that an input field or textarea doesn't contain the given value. + * For fuzzy locators, the field is matched by label text, CSS and XPath. + * + * ``` php + * dontSeeInField('Body','Type your comment here'); + * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); + * $I->dontSeeInField('form input[type=hidden]','hidden_value'); + * $I->dontSeeInField('#searchform input','Search'); + * $I->dontSeeInField('//form/*[@name=search]','Search'); + * $I->dontSeeInField(['name' => 'search'], 'Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeInField() + */ + public function cantSeeInField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInField', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that an input field or textarea doesn't contain the given value. + * For fuzzy locators, the field is matched by label text, CSS and XPath. + * + * ``` php + * dontSeeInField('Body','Type your comment here'); + * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); + * $I->dontSeeInField('form input[type=hidden]','hidden_value'); + * $I->dontSeeInField('#searchform input','Search'); + * $I->dontSeeInField('//form/*[@name=search]','Search'); + * $I->dontSeeInField(['name' => 'search'], 'Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see \Codeception\Lib\InnerBrowser::dontSeeInField() + */ + public function dontSeeInField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeInField', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if the array of form parameters (name => value) are set on the form matched with the + * passed selector. + * + * ``` php + * seeInFormFields('form[name=myform]', [ + * 'input1' => 'value', + * 'input2' => 'other value', + * ]); + * ?> + * ``` + * + * For multi-select elements, or to check values of multiple elements with the same name, an + * array may be passed: + * + * ``` php + * seeInFormFields('.form-class', [ + * 'multiselect' => [ + * 'value1', + * 'value2', + * ], + * 'checkbox[]' => [ + * 'a checked value', + * 'another checked value', + * ], + * ]); + * ?> + * ``` + * + * Additionally, checkbox values can be checked with a boolean. + * + * ``` php + * seeInFormFields('#form-id', [ + * 'checkbox1' => true, // passes if checked + * 'checkbox2' => false, // passes if unchecked + * ]); + * ?> + * ``` + * + * Pair this with submitForm for quick testing magic. + * + * ``` php + * 'value', + * 'field2' => 'another value', + * 'checkbox1' => true, + * // ... + * ]; + * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); + * // $I->amOnPage('/path/to/form-page') may be needed + * $I->seeInFormFields('//form[@id=my-form]', $form); + * ?> + * ``` + * + * @param $formSelector + * @param $params + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeInFormFields() + */ + public function canSeeInFormFields($formSelector, $params) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInFormFields', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if the array of form parameters (name => value) are set on the form matched with the + * passed selector. + * + * ``` php + * seeInFormFields('form[name=myform]', [ + * 'input1' => 'value', + * 'input2' => 'other value', + * ]); + * ?> + * ``` + * + * For multi-select elements, or to check values of multiple elements with the same name, an + * array may be passed: + * + * ``` php + * seeInFormFields('.form-class', [ + * 'multiselect' => [ + * 'value1', + * 'value2', + * ], + * 'checkbox[]' => [ + * 'a checked value', + * 'another checked value', + * ], + * ]); + * ?> + * ``` + * + * Additionally, checkbox values can be checked with a boolean. + * + * ``` php + * seeInFormFields('#form-id', [ + * 'checkbox1' => true, // passes if checked + * 'checkbox2' => false, // passes if unchecked + * ]); + * ?> + * ``` + * + * Pair this with submitForm for quick testing magic. + * + * ``` php + * 'value', + * 'field2' => 'another value', + * 'checkbox1' => true, + * // ... + * ]; + * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); + * // $I->amOnPage('/path/to/form-page') may be needed + * $I->seeInFormFields('//form[@id=my-form]', $form); + * ?> + * ``` + * + * @param $formSelector + * @param $params + * @see \Codeception\Lib\InnerBrowser::seeInFormFields() + */ + public function seeInFormFields($formSelector, $params) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInFormFields', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if the array of form parameters (name => value) are not set on the form matched with + * the passed selector. + * + * ``` php + * dontSeeInFormFields('form[name=myform]', [ + * 'input1' => 'non-existent value', + * 'input2' => 'other non-existent value', + * ]); + * ?> + * ``` + * + * To check that an element hasn't been assigned any one of many values, an array can be passed + * as the value: + * + * ``` php + * dontSeeInFormFields('.form-class', [ + * 'fieldName' => [ + * 'This value shouldn\'t be set', + * 'And this value shouldn\'t be set', + * ], + * ]); + * ?> + * ``` + * + * Additionally, checkbox values can be checked with a boolean. + * + * ``` php + * dontSeeInFormFields('#form-id', [ + * 'checkbox1' => true, // fails if checked + * 'checkbox2' => false, // fails if unchecked + * ]); + * ?> + * ``` + * + * @param $formSelector + * @param $params + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeInFormFields() + */ + public function cantSeeInFormFields($formSelector, $params) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInFormFields', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if the array of form parameters (name => value) are not set on the form matched with + * the passed selector. + * + * ``` php + * dontSeeInFormFields('form[name=myform]', [ + * 'input1' => 'non-existent value', + * 'input2' => 'other non-existent value', + * ]); + * ?> + * ``` + * + * To check that an element hasn't been assigned any one of many values, an array can be passed + * as the value: + * + * ``` php + * dontSeeInFormFields('.form-class', [ + * 'fieldName' => [ + * 'This value shouldn\'t be set', + * 'And this value shouldn\'t be set', + * ], + * ]); + * ?> + * ``` + * + * Additionally, checkbox values can be checked with a boolean. + * + * ``` php + * dontSeeInFormFields('#form-id', [ + * 'checkbox1' => true, // fails if checked + * 'checkbox2' => false, // fails if unchecked + * ]); + * ?> + * ``` + * + * @param $formSelector + * @param $params + * @see \Codeception\Lib\InnerBrowser::dontSeeInFormFields() + */ + public function dontSeeInFormFields($formSelector, $params) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeInFormFields', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Submits the given form on the page, with the given form + * values. Pass the form field's values as an array in the second + * parameter. + * + * Although this function can be used as a short-hand version of + * `fillField()`, `selectOption()`, `click()` etc. it has some important + * differences: + * + * * Only field *names* may be used, not CSS/XPath selectors nor field labels + * * If a field is sent to this function that does *not* exist on the page, + * it will silently be added to the HTTP request. This is helpful for testing + * some types of forms, but be aware that you will *not* get an exception + * like you would if you called `fillField()` or `selectOption()` with + * a missing field. + * + * Fields that are not provided will be filled by their values from the page, + * or from any previous calls to `fillField()`, `selectOption()` etc. + * You don't need to click the 'Submit' button afterwards. + * This command itself triggers the request to form's action. + * + * You can optionally specify which button's value to include + * in the request with the last parameter (as an alternative to + * explicitly setting its value in the second parameter), as + * button values are not otherwise included in the request. + * + * Examples: + * + * ``` php + * submitForm('#login', [ + * 'login' => 'davert', + * 'password' => '123456' + * ]); + * // or + * $I->submitForm('#login', [ + * 'login' => 'davert', + * 'password' => '123456' + * ], 'submitButtonName'); + * + * ``` + * + * For example, given this sample "Sign Up" form: + * + * ``` html + *
+ * Login: + *
+ * Password: + *
+ * Do you agree to our terms? + *
+ * Select pricing plan: + * + * + *
+ * ``` + * + * You could write the following to submit it: + * + * ``` php + * submitForm( + * '#userForm', + * [ + * 'user' => [ + * 'login' => 'Davert', + * 'password' => '123456', + * 'agree' => true + * ] + * ], + * 'submitButton' + * ); + * ``` + * Note that "2" will be the submitted value for the "plan" field, as it is + * the selected option. + * + * You can also emulate a JavaScript submission by not specifying any + * buttons in the third parameter to submitForm. + * + * ```php + * submitForm( + * '#userForm', + * [ + * 'user' => [ + * 'login' => 'Davert', + * 'password' => '123456', + * 'agree' => true + * ] + * ] + * ); + * ``` + * + * This function works well when paired with `seeInFormFields()` + * for quickly testing CRUD interfaces and form validation logic. + * + * ``` php + * 'value', + * 'field2' => 'another value', + * 'checkbox1' => true, + * // ... + * ]; + * $I->submitForm('#my-form', $form, 'submitButton'); + * // $I->amOnPage('/path/to/form-page') may be needed + * $I->seeInFormFields('#my-form', $form); + * ``` + * + * Parameter values can be set to arrays for multiple input fields + * of the same name, or multi-select combo boxes. For checkboxes, + * you can use either the string value or boolean `true`/`false` which will + * be replaced by the checkbox's value in the DOM. + * + * ``` php + * submitForm('#my-form', [ + * 'field1' => 'value', + * 'checkbox' => [ + * 'value of first checkbox', + * 'value of second checkbox', + * ], + * 'otherCheckboxes' => [ + * true, + * false, + * false + * ], + * 'multiselect' => [ + * 'first option value', + * 'second option value' + * ] + * ]); + * ``` + * + * Mixing string and boolean values for a checkbox's value is not supported + * and may produce unexpected results. + * + * Field names ending in `[]` must be passed without the trailing square + * bracket characters, and must contain an array for its value. This allows + * submitting multiple values with the same name, consider: + * + * ```php + * submitForm('#my-form', [ + * 'field[]' => 'value', + * 'field[]' => 'another value', // 'field[]' is already a defined key + * ]); + * ``` + * + * The solution is to pass an array value: + * + * ```php + * submitForm('#my-form', [ + * 'field' => [ + * 'value', + * 'another value', + * ] + * ]); + * ``` + * + * @param $selector + * @param $params + * @param $button + * @see \Codeception\Lib\InnerBrowser::submitForm() + */ + public function submitForm($selector, $params, $button = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('submitForm', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Fills a text field or textarea with the given string. + * + * ``` php + * fillField("//input[@type='text']", "Hello World!"); + * $I->fillField(['name' => 'email'], 'jon@mail.com'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see \Codeception\Lib\InnerBrowser::fillField() + */ + public function fillField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('fillField', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Selects an option in a select tag or in radio button group. + * + * ``` php + * selectOption('form select[name=account]', 'Premium'); + * $I->selectOption('form input[name=payment]', 'Monthly'); + * $I->selectOption('//form/select[@name=account]', 'Monthly'); + * ?> + * ``` + * + * Provide an array for the second argument to select multiple options: + * + * ``` php + * selectOption('Which OS do you use?', array('Windows','Linux')); + * ?> + * ``` + * + * Or provide an associative array for the second argument to specifically define which selection method should be used: + * + * ``` php + * selectOption('Which OS do you use?', array('text' => 'Windows')); // Only search by text 'Windows' + * $I->selectOption('Which OS do you use?', array('value' => 'windows')); // Only search by value 'windows' + * ?> + * ``` + * + * @param $select + * @param $option + * @see \Codeception\Lib\InnerBrowser::selectOption() + */ + public function selectOption($select, $option) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('selectOption', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Ticks a checkbox. For radio buttons, use the `selectOption` method instead. + * + * ``` php + * checkOption('#agree'); + * ?> + * ``` + * + * @param $option + * @see \Codeception\Lib\InnerBrowser::checkOption() + */ + public function checkOption($option) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('checkOption', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Unticks a checkbox. + * + * ``` php + * uncheckOption('#notify'); + * ?> + * ``` + * + * @param $option + * @see \Codeception\Lib\InnerBrowser::uncheckOption() + */ + public function uncheckOption($option) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('uncheckOption', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Attaches a file relative to the Codeception `_data` directory to the given file upload field. + * + * ``` php + * attachFile('input[@type="file"]', 'prices.xls'); + * ?> + * ``` + * + * @param $field + * @param $filename + * @see \Codeception\Lib\InnerBrowser::attachFile() + */ + public function attachFile($field, $filename) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('attachFile', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * If your page triggers an ajax request, you can perform it manually. + * This action sends a GET ajax request with specified params. + * + * See ->sendAjaxPostRequest for examples. + * + * @param $uri + * @param $params + * @see \Codeception\Lib\InnerBrowser::sendAjaxGetRequest() + */ + public function sendAjaxGetRequest($uri, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendAjaxGetRequest', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * If your page triggers an ajax request, you can perform it manually. + * This action sends a POST ajax request with specified params. + * Additional params can be passed as array. + * + * Example: + * + * Imagine that by clicking checkbox you trigger ajax request which updates user settings. + * We emulate that click by running this ajax request manually. + * + * ``` php + * sendAjaxPostRequest('/updateSettings', array('notifications' => true)); // POST + * $I->sendAjaxGetRequest('/updateSettings', array('notifications' => true)); // GET + * + * ``` + * + * @param $uri + * @param $params + * @see \Codeception\Lib\InnerBrowser::sendAjaxPostRequest() + */ + public function sendAjaxPostRequest($uri, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendAjaxPostRequest', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * If your page triggers an ajax request, you can perform it manually. + * This action sends an ajax request with specified method and params. + * + * Example: + * + * You need to perform an ajax request specifying the HTTP method. + * + * ``` php + * sendAjaxRequest('PUT', '/posts/7', array('title' => 'new title')); + * + * ``` + * + * @param $method + * @param $uri + * @param $params + * @see \Codeception\Lib\InnerBrowser::sendAjaxRequest() + */ + public function sendAjaxRequest($method, $uri, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendAjaxRequest', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Finds and returns the text contents of the given element. + * If a fuzzy locator is used, the element is found using CSS, XPath, + * and by matching the full page source by regular expression. + * + * ``` php + * grabTextFrom('h1'); + * $heading = $I->grabTextFrom('descendant-or-self::h1'); + * $value = $I->grabTextFrom('~ + * ``` + * + * @param $cssOrXPathOrRegex + * + * @return mixed + * @see \Codeception\Lib\InnerBrowser::grabTextFrom() + */ + public function grabTextFrom($cssOrXPathOrRegex) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabTextFrom', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Grabs the value of the given attribute value from the given element. + * Fails if element is not found. + * + * ``` php + * grabAttributeFrom('#tooltip', 'title'); + * ?> + * ``` + * + * + * @param $cssOrXpath + * @param $attribute + * + * @return mixed + * @see \Codeception\Lib\InnerBrowser::grabAttributeFrom() + */ + public function grabAttributeFrom($cssOrXpath, $attribute) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabAttributeFrom', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Grabs either the text content, or attribute values, of nodes + * matched by $cssOrXpath and returns them as an array. + * + * ```html + * First + * Second + * Third + * ``` + * + * ```php + * grabMultiple('a'); + * + * // would return ['#first', '#second', '#third'] + * $aLinks = $I->grabMultiple('a', 'href'); + * ?> + * ``` + * + * @param $cssOrXpath + * @param $attribute + * @return string[] + * @see \Codeception\Lib\InnerBrowser::grabMultiple() + */ + public function grabMultiple($cssOrXpath, $attribute = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabMultiple', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $field + * + * @return array|mixed|null|string + * @see \Codeception\Lib\InnerBrowser::grabValueFrom() + */ + public function grabValueFrom($field) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabValueFrom', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sets a cookie with the given name and value. + * You can set additional cookie params like `domain`, `path`, `expires`, `secure` in array passed as last argument. + * + * ``` php + * setCookie('PHPSESSID', 'el4ukv0kqbvoirg7nkp4dncpk3'); + * ?> + * ``` + * + * @param $name + * @param $val + * @param array $params + * + * @return mixed + * @see \Codeception\Lib\InnerBrowser::setCookie() + */ + public function setCookie($name, $val, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('setCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Grabs a cookie value. + * You can set additional cookie params like `domain`, `path` in array passed as last argument. + * + * @param $cookie + * + * @param array $params + * @return mixed + * @see \Codeception\Lib\InnerBrowser::grabCookie() + */ + public function grabCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Grabs current page source code. + * + * @throws ModuleException if no page was opened. + * + * @return string Current page source code. + * @see \Codeception\Lib\InnerBrowser::grabPageSource() + */ + public function grabPageSource() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabPageSource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that a cookie with the given name is set. + * You can set additional cookie params like `domain`, `path` as array passed in last argument. + * + * ``` php + * seeCookie('PHPSESSID'); + * ?> + * ``` + * + * @param $cookie + * @param array $params + * @return mixed + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeCookie() + */ + public function canSeeCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeCookie', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that a cookie with the given name is set. + * You can set additional cookie params like `domain`, `path` as array passed in last argument. + * + * ``` php + * seeCookie('PHPSESSID'); + * ?> + * ``` + * + * @param $cookie + * @param array $params + * @return mixed + * @see \Codeception\Lib\InnerBrowser::seeCookie() + */ + public function seeCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there isn't a cookie with the given name. + * You can set additional cookie params like `domain`, `path` as array passed in last argument. + * + * @param $cookie + * + * @param array $params + * @return mixed + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeCookie() + */ + public function cantSeeCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeCookie', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there isn't a cookie with the given name. + * You can set additional cookie params like `domain`, `path` as array passed in last argument. + * + * @param $cookie + * + * @param array $params + * @return mixed + * @see \Codeception\Lib\InnerBrowser::dontSeeCookie() + */ + public function dontSeeCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Unsets cookie with the given name. + * You can set additional cookie params like `domain`, `path` in array passed as last argument. + * + * @param $cookie + * + * @param array $params + * @return mixed + * @see \Codeception\Lib\InnerBrowser::resetCookie() + */ + public function resetCookie($name, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('resetCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given element exists on the page and is visible. + * You can also specify expected attributes of this element. + * + * ``` php + * seeElement('.error'); + * $I->seeElement('//form/input[1]'); + * $I->seeElement('input', ['name' => 'login']); + * $I->seeElement('input', ['value' => '123456']); + * + * // strict locator in first arg, attributes in second + * $I->seeElement(['css' => 'form input'], ['name' => 'login']); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @return + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeElement() + */ + public function canSeeElement($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeElement', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given element exists on the page and is visible. + * You can also specify expected attributes of this element. + * + * ``` php + * seeElement('.error'); + * $I->seeElement('//form/input[1]'); + * $I->seeElement('input', ['name' => 'login']); + * $I->seeElement('input', ['value' => '123456']); + * + * // strict locator in first arg, attributes in second + * $I->seeElement(['css' => 'form input'], ['name' => 'login']); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @return + * @see \Codeception\Lib\InnerBrowser::seeElement() + */ + public function seeElement($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeElement', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given element is invisible or not present on the page. + * You can also specify expected attributes of this element. + * + * ``` php + * dontSeeElement('.error'); + * $I->dontSeeElement('//form/input[1]'); + * $I->dontSeeElement('input', ['name' => 'login']); + * $I->dontSeeElement('input', ['value' => '123456']); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeElement() + */ + public function cantSeeElement($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeElement', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given element is invisible or not present on the page. + * You can also specify expected attributes of this element. + * + * ``` php + * dontSeeElement('.error'); + * $I->dontSeeElement('//form/input[1]'); + * $I->dontSeeElement('input', ['name' => 'login']); + * $I->dontSeeElement('input', ['value' => '123456']); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @see \Codeception\Lib\InnerBrowser::dontSeeElement() + */ + public function dontSeeElement($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeElement', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there are a certain number of elements matched by the given locator on the page. + * + * ``` php + * seeNumberOfElements('tr', 10); + * $I->seeNumberOfElements('tr', [0,10]); // between 0 and 10 elements + * ?> + * ``` + * @param $selector + * @param mixed $expected int or int[] + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeNumberOfElements() + */ + public function canSeeNumberOfElements($selector, $expected) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeNumberOfElements', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there are a certain number of elements matched by the given locator on the page. + * + * ``` php + * seeNumberOfElements('tr', 10); + * $I->seeNumberOfElements('tr', [0,10]); // between 0 and 10 elements + * ?> + * ``` + * @param $selector + * @param mixed $expected int or int[] + * @see \Codeception\Lib\InnerBrowser::seeNumberOfElements() + */ + public function seeNumberOfElements($selector, $expected) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeNumberOfElements', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given option is selected. + * + * ``` php + * seeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * + * @return mixed + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeOptionIsSelected() + */ + public function canSeeOptionIsSelected($selector, $optionText) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeOptionIsSelected', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given option is selected. + * + * ``` php + * seeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * + * @return mixed + * @see \Codeception\Lib\InnerBrowser::seeOptionIsSelected() + */ + public function seeOptionIsSelected($selector, $optionText) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeOptionIsSelected', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given option is not selected. + * + * ``` php + * dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * + * @return mixed + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeOptionIsSelected() + */ + public function cantSeeOptionIsSelected($selector, $optionText) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeOptionIsSelected', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given option is not selected. + * + * ``` php + * dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * + * @return mixed + * @see \Codeception\Lib\InnerBrowser::dontSeeOptionIsSelected() + */ + public function dontSeeOptionIsSelected($selector, $optionText) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeOptionIsSelected', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Asserts that current page has 404 response status code. + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seePageNotFound() + */ + public function canSeePageNotFound() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seePageNotFound', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Asserts that current page has 404 response status code. + * @see \Codeception\Lib\InnerBrowser::seePageNotFound() + */ + public function seePageNotFound() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seePageNotFound', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that response code is equal to value provided. + * + * ```php + * seeResponseCodeIs(200); + * + * // recommended \Codeception\Util\HttpCode + * $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * + * @param $code + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIs() + */ + public function canSeeResponseCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIs', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that response code is equal to value provided. + * + * ```php + * seeResponseCodeIs(200); + * + * // recommended \Codeception\Util\HttpCode + * $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * + * @param $code + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIs() + */ + public function seeResponseCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIs', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that response code is between a certain range. Between actually means [from <= CODE <= to] + * + * @param $from + * @param $to + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsBetween() + */ + public function canSeeResponseCodeIsBetween($from, $to) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsBetween', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that response code is between a certain range. Between actually means [from <= CODE <= to] + * + * @param $from + * @param $to + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsBetween() + */ + public function seeResponseCodeIsBetween($from, $to) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsBetween', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that response code is equal to value provided. + * + * ```php + * dontSeeResponseCodeIs(200); + * + * // recommended \Codeception\Util\HttpCode + * $I->dontSeeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * @param $code + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeResponseCodeIs() + */ + public function cantSeeResponseCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeResponseCodeIs', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that response code is equal to value provided. + * + * ```php + * dontSeeResponseCodeIs(200); + * + * // recommended \Codeception\Util\HttpCode + * $I->dontSeeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * @param $code + * @see \Codeception\Lib\InnerBrowser::dontSeeResponseCodeIs() + */ + public function dontSeeResponseCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeResponseCodeIs', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code 2xx + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsSuccessful() + */ + public function canSeeResponseCodeIsSuccessful() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsSuccessful', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code 2xx + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsSuccessful() + */ + public function seeResponseCodeIsSuccessful() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsSuccessful', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code 3xx + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsRedirection() + */ + public function canSeeResponseCodeIsRedirection() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsRedirection', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code 3xx + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsRedirection() + */ + public function seeResponseCodeIsRedirection() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsRedirection', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code is 4xx + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsClientError() + */ + public function canSeeResponseCodeIsClientError() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsClientError', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code is 4xx + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsClientError() + */ + public function seeResponseCodeIsClientError() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsClientError', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code is 5xx + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsServerError() + */ + public function canSeeResponseCodeIsServerError() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsServerError', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code is 5xx + * @see \Codeception\Lib\InnerBrowser::seeResponseCodeIsServerError() + */ + public function seeResponseCodeIsServerError() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsServerError', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page title contains the given string. + * + * ``` php + * seeInTitle('Blog - Post #1'); + * ?> + * ``` + * + * @param $title + * + * @return mixed + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::seeInTitle() + */ + public function canSeeInTitle($title) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInTitle', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page title contains the given string. + * + * ``` php + * seeInTitle('Blog - Post #1'); + * ?> + * ``` + * + * @param $title + * + * @return mixed + * @see \Codeception\Lib\InnerBrowser::seeInTitle() + */ + public function seeInTitle($title) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInTitle', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page title does not contain the given string. + * + * @param $title + * + * @return mixed + * Conditional Assertion: Test won't be stopped on fail + * @see \Codeception\Lib\InnerBrowser::dontSeeInTitle() + */ + public function cantSeeInTitle($title) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInTitle', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page title does not contain the given string. + * + * @param $title + * + * @return mixed + * @see \Codeception\Lib\InnerBrowser::dontSeeInTitle() + */ + public function dontSeeInTitle($title) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('dontSeeInTitle', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Switch to iframe or frame on the page. + * + * Example: + * ``` html + *