%PDF- %GIF98; %PNG;
Server : ApacheSystem : Linux host.digitalbabaji.in 4.18.0-513.11.1.el8_9.x86_64 #1 SMP Wed Jan 17 02:00:40 EST 2024 x86_64 User : addictionfreeind ( 1003) PHP Version : 7.2.34 Disable Function : exec,passthru,shell_exec,system Directory : /home/addictionfreeind/public_html/admin1/vendor/froiden/laravel-rest-api/src/ |
Upload File : |
<?php
namespace Froiden\RestAPI;
use Froiden\RestAPI\Exceptions\Parse\InvalidLimitException;
use Froiden\RestAPI\Exceptions\Parse\InvalidFilterDefinitionException;
use Froiden\RestAPI\Exceptions\Parse\InvalidOrderingDefinitionException;
use Froiden\RestAPI\Exceptions\Parse\MaxLimitException;
use Froiden\RestAPI\Exceptions\Parse\NotAllowedToFilterOnThisFieldException;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Str;
class RequestParser
{
/**
* Checks if fields are specified correctly
*/
const FIELDS_REGEX = "/([a-zA-Z0-9\\_\\-\\:\\.\\(\\)]+(?'curly'\\{((?>[^{}]+)|(?&curly))*\\})?+)/";
/**
* Extracts fields parts
*/
const FIELD_PARTS_REGEX = "/([^{.]+)(.limit\\(([0-9]+)\\)|.offset\\(([0-9]+)\\)|.order\\(([A-Za-z_]+)\\))*(\\{((?>[^{}]+)|(?R))*\\})?/i";
/**
* Checks if filters are correctly specified
*/
const FILTER_REGEX = "/(\\((?:[\\s]*(?:and|or)?[\\s]*[\\w\\.]+[\\s]+(?:eq|ne|gt|ge|lt|le|lk)[\\s]+(?:\\\"(?:[^\\\"\\\\]|\\\\.)*\\\"|\\d+(,\\d+)*(\\.\\d+(e\\d+)?)?|null)[\\s]*|(?R))*\\))/i";
/**
* Extracts filter parts
*/
const FILTER_PARTS_REGEX = "/([\\w\\.]+)[\\s]+(?:eq|ne|gt|ge|lt|le|lk)[\\s]+(?:\"(?:[^\"\\\\]|\\\\.)*\"|\\d+(?:,\\d+)*(?:\\.\\d+(?:e\\d+)?)?|null)/i";
/**
* Checks if ordering is specified correctly
*/
const ORDER_FILTER = "/[\\s]*([\\w\\.]+)(?:[\\s](?!,))*(asc|desc|)/i";
/**
* Full class reference to model this controller represents
*
* @var string
*/
protected $model = null;
/**
* Table name corresponding to the model this controller is handling
*
* @var string
*/
private $table = null;
/**
* Primary key of the model
*
* @var string
*/
private $primaryKey = null;
/**
* Fields to be returned in response. This does not include relations
*
* @var array
*/
private $fields = [];
/**
* Relations to be included in the response
*
* @var array
*/
private $relations = [];
/**
* Number of results requested per page
*
* @var int
*/
private $limit = 10;
/**
* Offset from where fetching should start
*
* @var int
*/
private $offset = 0;
/**
* Ordering string
*
* @var int
*/
private $order = null;
/**
* Filters to be applied
*
* @var string
*/
private $filters = null;
/**
* Attributes passed in request
*
* @var array
*/
private $attributes = [];
public function __construct($model)
{
$this->model = $model;
$this->primaryKey = call_user_func([new $this->model(), "getKeyName"]);
$this->parseRequest();
}
/**
* @return array
*/
public function getFields()
{
return $this->fields;
}
/**
* @param array $fields
*/
public function setFields($fields)
{
$this->fields = $fields;
}
/**
* @return array
*/
public function getRelations()
{
return $this->relations;
}
/**
* @param array $relations
*/
public function setRelations($relations)
{
$this->relations = $relations;
}
/**
* @return int
*/
public function getLimit()
{
return $this->limit;
}
/**
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* @return int
*/
public function getOrder()
{
return $this->order;
}
/**
* @return string
*/
public function getFilters()
{
return $this->filters;
}
/**
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Parse request and fill the parameters
* @return $this current controller object for chain method calling
* @throws InvalidFilterDefinitionException
* @throws InvalidOrderingDefinitionException
* @throws MaxLimitException
*/
protected function parseRequest()
{
if (request()->limit) {
if (request()->limit <= 0) {
throw new InvalidLimitException();
}
else if (request()->limit > config("api.maxLimit")) {
throw new MaxLimitException();
}
else {
$this->limit = request()->limit;
}
}
else {
$this->limit = config("api.defaultLimit");
}
if (request()->offset) {
$this->offset = request()->offset;
}
else {
$this->offset = 0;
}
$this->extractFields();
$this->extractFilters();
$this->extractOrdering();
$this->loadTableName();
$this->attributes = request()->all();
return $this;
}
protected function extractFields()
{
if (request()->fields) {
$this->parseFields(request()->fields);
}
else {
// Else, by default, we only return default set of visible fields
$fields = call_user_func($this->model."::getDefaultFields");
// We parse the default fields in same way as above so that, if
// relations are included in default fields, they also get included
$this->parseFields(implode(",", $fields));
}
if (!in_array($this->primaryKey, $this->fields)) {
$this->fields[] = $this->primaryKey;
}
}
protected function extractFilters()
{
if (request()->filters) {
$filters = "(" . request()->filters . ")";
if (preg_match(RequestParser::FILTER_REGEX, $filters) === 1) {
preg_match_all(RequestParser::FILTER_PARTS_REGEX, $filters, $parts);
$filterable = call_user_func($this->model . "::getFilterableFields");
foreach ($parts[1] as $column) {
if (!in_array($column, $filterable)) {
throw new NotAllowedToFilterOnThisFieldException("Applying filter on field \"" . $column . "\" is not allowed");
}
}
// Convert filter name to sql `column` format
$where = preg_replace(
[
"/([\\w]+)\\.([\\w]+)[\\s]+(eq|ne|gt|ge|lt|le|lk)/i",
"/([\\w]+)[\\s]+(eq|ne|gt|ge|lt|le|lk)/i",
],
[
"`$1`.`$2` $3",
"`$1` $2",
],
$filters
);
// convert eq null to is null and ne null to is not null
$where = preg_replace(
[
"/ne[\\s]+null/i",
"/eq[\\s]+null/i"
],
[
"is not null",
"is null"
],
$where
);
// Replace operators
$where = preg_replace(
[
"/[\\s]+eq[\\s]+/i",
"/[\\s]+ne[\\s]+/i",
"/[\\s]+gt[\\s]+/i",
"/[\\s]+ge[\\s]+/i",
"/[\\s]+lt[\\s]+/i",
"/[\\s]+le[\\s]+/i",
"/[\\s]+lk[\\s]+/i"
],
[
" = ",
" != ",
" > ",
" >= ",
" < ",
" <= ",
" LIKE "
],
$where
);
$this->filters = $where;
}
else {
throw new InvalidFilterDefinitionException();
}
}
}
protected function extractOrdering()
{
if (request()->order) {
if (preg_match(RequestParser::ORDER_FILTER, request()->order) === 1) {
$order = request()->order;
// eg : user.name asc, year desc, age,month
$order = preg_replace(
[
"/[\\s]*([\\w]+)\\.([\\w]+)(?:[\\s](?!,))*(asc|desc|)/",
"/[\\s]*([\\w`\\.]+)(?:[\\s](?!,))*(asc|desc|)/",
],
[
"$1`.`$2 $3", // Result: user`.`name asc, year desc, age,month
"`$1` $2", // Result: `user`.`name` asc, `year` desc, `age`,`month`
],
$order
);
$this->order = $order;
}
else {
throw new InvalidOrderingDefinitionException();
}
}
}
/**
* Recursively parses fields to extract limit, ordering and their own fields
* and adds width relations
*
* @param $fields
*/
private function parseFields($fields)
{
// If fields parameter is set, parse it using regex
preg_match_all(static::FIELDS_REGEX, $fields, $matches);
if (!empty($matches[0])) {
foreach ($matches[0] as $match) {
preg_match_all(static::FIELD_PARTS_REGEX, $match, $parts);
$fieldName = $parts[1][0];
if (Str::contains($fieldName, ":") || call_user_func($this->model . "::relationExists", $fieldName)) {
// If field name has a colon, we assume its a relations
// OR
// If method with field name exists in the class, we assume its a relation
// This is default laravel behavior
$limit = ($parts[3][0] == "") ? config("api.defaultLimit") : $parts[3][0];
$offset = ($parts[4][0] == "") ? 0 : $parts[4][0];
$order = ($parts[5][0] == "chronological") ? "chronological" : "reverse_chronological";
if (!empty($parts[7][0])) {
$subFields = explode(",", $parts[7][0]);
// This indicates if user specified fields for relation or not
$userSpecifiedFields = true;
}
else {
$subFields = [];
$userSpecifiedFields = false;
}
$fieldName = str_replace(":", ".", $fieldName);
// Check if relation name in modal is in camel case then convert relation name in camel case
if(config("api.relation_case", 'snakecase') === 'camelcase'){
$fieldName = Str::camel($fieldName);
}
if (!isset($this->relations[$fieldName])) {
$this->relations[$fieldName] = [
"limit" => $limit,
"offset" => $offset,
"order" => $order,
"fields" => $subFields,
"userSpecifiedFields" => $userSpecifiedFields
];
}
else {
$this->relations[$fieldName]["limit"] = $limit;
$this->relations[$fieldName]["offset"] = $offset;
$this->relations[$fieldName]["order"] = $order;
$this->relations[$fieldName]["fields"] = array_merge($this->relations[$fieldName]["fields"], $subFields);
}
// We also need to add the relation's foreign key field to select. If we don't,
// relations always return null
if (Str::contains($fieldName, ".")) {
$relationNameParts = explode('.', $fieldName);
$model = $this->model;
$relation = null;
foreach ($relationNameParts as $rp) {
$relation = call_user_func([ new $model(), $rp]);
$model = $relation->getRelated();
}
// Its a multi level relations
$fieldParts = explode(".", $fieldName);
if ($relation instanceof BelongsTo) {
$singular = $relation->getForeignKeyName();
}
else if ($relation instanceof HasOne || $relation instanceof HasMany) {
$singular = $relation->getForeignKeyName();
}
// Unset last element of array
unset($fieldParts[count($fieldParts) - 1]);
$parent = implode(".", $fieldParts);
if ($relation instanceof HasOne || $relation instanceof HasMany) {
// For hasMany and HasOne, the foreign key is in current relation table, not in parent
$this->relations[$fieldName]["fields"][] = $singular;
}
else {
// The parent might already been set because we cannot rely on order
// in which user sends relations in request
if (!isset($this->relations[$parent])) {
$this->relations[$parent] = [
"limit" => config("api.defaultLimit"),
"offset" => 0,
"order" => "chronological",
"fields" => isset($singular) ? [$singular] : [],
"userSpecifiedFields" => true
];
}
else {
if (isset($singular)) {
$this->relations[$parent]["fields"][] = $singular;
}
}
}
if ($relation instanceof BelongsTo) {
$this->relations[$fieldName]["limit"] = max($this->relations[$fieldName]["limit"], $this->relations[$parent]["limit"]);
}
else if ($relation instanceof HasMany) {
$this->relations[$fieldName]["limit"] = $this->relations[$fieldName]["limit"] * $this->relations[$parent]["limit"];
}
}
else {
$relation = call_user_func([new $this->model(), $fieldName]);
if ($relation instanceof HasOne) {
$keyField = explode(".", $relation->getQualifiedParentKeyName())[1];
}
else if ($relation instanceof BelongsTo) {
$keyField = explode(".", $relation->getQualifiedForeignKeyName())[1];
}
if (isset($keyField) && !in_array($keyField, $this->fields)) {
$this->fields[] = $keyField;
}
if ($relation instanceof BelongsTo) {
$this->relations[$fieldName]["limit"] = max($this->relations[$fieldName]["limit"], $this->limit);
}
else if ($relation instanceof HasMany) {
// Commented out for third level hasmany limit
// $this->relations[$fieldName]["limit"] = $this->relations[$fieldName]["limit"] * $this->limit;
}
}
}
else {
// Else, its a normal field
$this->fields[] = $fieldName;
}
}
}
}
/**
* Load table name into the $table property
*/
private function loadTableName()
{
$this->table = call_user_func($this->model."::getTableName");
}
}