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 + *