This commit is contained in:
root
2025-11-13 19:52:28 +03:00
parent 8aeeb05b7d
commit 807dec3b6c
4646 changed files with 163445 additions and 626017 deletions

View File

@@ -0,0 +1,8 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
abstract class AbstractAttribute
{
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class DtoType extends AbstractAttribute
{
public function __construct(public readonly string $type)
{
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class Editable extends AbstractAttribute
{
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class ElementType extends AbstractAttribute
{
public function __construct(public readonly string $type)
{
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class Filterable extends AbstractAttribute
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class JsonArray extends AbstractAttribute
{
}

View File

@@ -0,0 +1,8 @@
<?php
use Bitrix\Rest\V3\Attributes\AbstractAttribute;
#[\Attribute]
class Nullable extends AbstractAttribute
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class Optional extends AbstractAttribute
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
use Bitrix\Main\Entity\DataManager;
use Bitrix\Rest\V3\Exceptions\InvalidClassInstanceProvidedException;
#[\Attribute]
class OrmEntity extends AbstractAttribute
{
public function __construct(public readonly string $entity)
{
if (!is_subclass_of($this->entity, DataManager::class))
{
throw new InvalidClassInstanceProvidedException($this->entity, DataManager::class);
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class RelationToMany
{
/**
* @param string $thisField
* @param string $refField
* @param array{
* string: array<string, string>
* } $sort
* @example
* $sort = ['order' => ['id' => 'desc']];
*/
public function __construct(
public readonly string $thisField,
public readonly string $refField,
public readonly array $sort
)
{
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class RelationToOne extends AbstractAttribute
{
/**
* @param string $thisField
* @param string $refField
*/
public function __construct(
public readonly string $thisField,
public readonly string $refField
) {}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
use Bitrix\Rest\V3\Controllers\RestController;
use Bitrix\Rest\V3\Exceptions\InvalidClassInstanceProvidedException;
#[\Attribute]
class ResolvedBy extends AbstractAttribute
{
public function __construct(public readonly string $controller)
{
if (!is_subclass_of($this->controller, RestController::class))
{
throw new InvalidClassInstanceProvidedException($this->controller, RestController::class);
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class Scope extends AbstractAttribute
{
public function __construct(public readonly string $value)
{
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Bitrix\Rest\V3\Attributes;
#[\Attribute]
class Sortable extends AbstractAttribute
{
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Bitrix\Rest\V3;
use Bitrix\Main\Application;
final class CacheManager
{
private const CACHE_DIR = 'rest/v3';
private const CACHE_TTL = 31536000; // One year TTL
public static function get(string $key): mixed
{
$cache = Application::getInstance()->getManagedCache();
if ($cache->read(self::CACHE_TTL, $key, self::CACHE_DIR))
{
return $cache->get($key);
}
return null;
}
public static function set(string $key, mixed $value): bool
{
$cache = Application::getInstance()->getManagedCache();
$cache->read(self::CACHE_TTL, $key, self::CACHE_DIR);
$cache->setImmediate($key, $value);
return true;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Bitrix\Rest\V3\Controllers\ActionFilter;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use Bitrix\Rest\V3\Exceptions\AccessDeniedException;
class UserCanDoOperation extends \Bitrix\Rest\Engine\ActionFilter\UserCanDoOperation
{
public function onBeforeAction(Event $event)
{
global $USER;
foreach ($this->operations as $operation)
{
if (!$USER->CanDoOperation($operation))
{
throw new AccessDeniedException();
}
}
return new EventResult(EventResult::SUCCESS, null, null, $this);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Interaction\Request\AddRequest;
use Bitrix\Rest\V3\Interaction\Response\AddResponse;
interface AddActionInterface
{
public function addAction(AddRequest $request): AddResponse;
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Exceptions\Validation\DtoValidationException;
use Bitrix\Rest\V3\Interaction\Request\AddRequest;
use Bitrix\Rest\V3\Interaction\Response\AddResponse;
trait AddOrmActionTrait
{
use OrmActionTrait;
use ValidateDtoTrait;
final public function addAction(AddRequest $request): AddResponse
{
// convert fields to dto
$dto = $request->fields->getAsDto();
// validate
if (!$this->validateDto($dto))
{
throw new DtoValidationException($this->getErrors());
}
$repository = $this->getOrmRepositoryByRequest($request);
$response = $repository->add($dto);
return new AddResponse($response);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Interaction\Request\AggregateRequest;
use Bitrix\Rest\V3\Interaction\Response\AggregateResponse;
interface AggregateActionInterface
{
public function aggregateAction(AggregateRequest $request): AggregateResponse;
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\SystemException;
use Bitrix\Rest\V3\Exceptions\ClassRequireAttributeException;
use Bitrix\Rest\V3\Interaction\Request\AggregateRequest;
use Bitrix\Rest\V3\Interaction\Response\AggregateResponse;
trait AggregateOrmActionTrait
{
use OrmActionTrait;
/**
* @throws SystemException
* @throws ArgumentException
* @throws ClassRequireAttributeException
*/
final public function aggregateAction(AggregateRequest $request): AggregateResponse
{
$repository = $this->getOrmRepositoryByRequest($request);
$result = $repository->getAllWithAggregate($request->select, $request->filter);
return new AggregateResponse($result);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Interaction\Request\DeleteRequest;
use Bitrix\Rest\V3\Interaction\Response\DeleteResponse;
interface DeleteActionInterface
{
public function deleteAction(DeleteRequest $request): DeleteResponse;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Exceptions\Validation\RequiredFieldInRequestException;
use Bitrix\Rest\V3\Interaction\Request\DeleteRequest;
use Bitrix\Rest\V3\Interaction\Response\DeleteResponse;
trait DeleteOrmActionTrait
{
use OrmActionTrait;
final public function deleteAction(DeleteRequest $request): DeleteResponse
{
if ($request->id === null && $request->filter === null)
{
throw new RequiredFieldInRequestException('id || filter');
}
$repository = $this->getOrmRepositoryByRequest($request);
$result = $request->id !== null ? $repository->delete($request->id) : $repository->deleteMulti($request->filter);
return new DeleteResponse($result);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Interaction\Request\GetRequest;
use Bitrix\Rest\V3\Interaction\Response\GetResponse;
interface GetActionInterface
{
public function getAction(GetRequest $request): GetResponse;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Exceptions\EntityNotFoundException;
use Bitrix\Rest\V3\Interaction\Request\GetRequest;
use Bitrix\Rest\V3\Interaction\Response\GetResponse;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
trait GetOrmActionTrait
{
use OrmActionTrait;
final public function getAction(GetRequest $request): GetResponse
{
$dto = $this->getOrmRepositoryByRequest($request)->getOneWith($request->select, (new FilterStructure())->where('id', $request->id));
if ($dto === null)
{
throw new EntityNotFoundException($request->id);
}
return new GetResponse($dto);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\ObjectPropertyException;
use Bitrix\Main\SystemException;
use Bitrix\Rest\V3\Interaction\Request\ListRequest;
use Bitrix\Rest\V3\Interaction\Response\ListResponse;
interface ListActionInterface
{
/**
* @throws ObjectPropertyException
* @throws SystemException
* @throws ArgumentException
*/
public function listAction(ListRequest $request): ListResponse;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Interaction\Request\ListRequest;
use Bitrix\Rest\V3\Interaction\Response\ListResponse;
trait ListOrmActionTrait
{
use OrmActionTrait;
final public function listAction(ListRequest $request): ListResponse
{
$collection = $this->getOrmRepositoryByRequest($request)->getAll(
$request->select,
$request->filter,
$request->order,
$request->pagination,
);
return new ListResponse($collection);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Data\OrmRepository;
use Bitrix\Rest\V3\Interaction\Request\Request;
trait OrmActionTrait
{
public function getOrmRepositoryByRequest(Request $request): OrmRepository
{
return new OrmRepository($request->getDtoClass());
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Main\Engine\Action;
use Bitrix\Main\Engine\AutoWire\Parameter;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Error;
use Bitrix\Rest\V3\Attributes\DtoType;
use Bitrix\Rest\V3\Exceptions\ClassRequireAttributeException;
use Bitrix\Rest\V3\Exceptions\Internal\InternalException;
use Bitrix\Rest\V3\Exceptions\RestException;
use Bitrix\Rest\V3\Exceptions\SkipWriteToLogException;
use Bitrix\Rest\V3\Exceptions\TooManyAttributesException;
use Bitrix\Rest\V3\Interaction\Request\Request;
use Bitrix\Rest\V3\Interaction\Response\Response;
use Bitrix\Rest\V3\Interaction\Response\ResponseWithRelations;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
use Throwable;
abstract class RestController extends Controller
{
protected ?string $localErrorLanguage = null;
public function getAutoWiredParameters(): array
{
return [
new Parameter(
Request::class,
function(string $className)
{
/** @var Request $className */
$controllerReflection = new \ReflectionClass($this);
$attributes = $controllerReflection->getAttributes(DtoType::class);
// check if there is any dto in controller
if (!empty($attributes))
{
if (count($attributes) > 1)
{
throw new TooManyAttributesException($className, DtoType::class, 1);
}
/** @var DtoType $dtoTypeAttribute */
$dtoTypeAttribute = $attributes[0]->newInstance();
$dtoType = $dtoTypeAttribute->type;
}
else
{
throw new ClassRequireAttributeException(get_class($this), DtoType::class);
}
// create request
return $className::create($this->getRequest(), $dtoType);
},
),
];
}
public function setLocalErrorLanguage(?string $localErrorLanguage): void
{
$this->localErrorLanguage = $localErrorLanguage;
}
protected function getActionResponse(Action $action)
{
$response = $action->runWithSourceParametersList();
if ($response instanceof ResponseWithRelations)
{
$args = $action->getArguments();
$request = $args['request'];
$response->setParentRequest($request);
$this->updateRequestRelationFilters($request, $response);
if (!empty($request->getRelations()))
{
$response->setRelations($request->getRelations());
}
}
return $response;
}
private function updateRequestRelationFilters(Request $request, Response $response): void
{
if (!$request->select)
{
return;
}
$relationFields = $request->select->getRelationFields();
$relationFilterValues = $this->getResultRelationFilterValues($relationFields, $response);
foreach ($request->getRelations() as $relation)
{
if (isset($relationFilterValues[$relation->getFromField()]))
{
$relationFilter = FilterStructure::create([$relation->getToField(), $relationFilterValues[$relation->getFromField()]], $relation->getRequest()->getDtoClass(), $relation->getRequest());
$relation->getRequest()->filter = $relationFilter;
}
}
}
private function getResultRelationFilterValues(array $relationFields, Response $response): array
{
$result = [];
if (isset($response->items))
{
foreach ($response->items as $item)
{
$this->fillResultRelationFilterField($item, $relationFields, $result);
}
}
else
{
$this->fillResultRelationFilterField($response->item, $relationFields, $result);
}
return $result;
}
private function fillResultRelationFilterField(array $item, array $relationFields, array &$result): void
{
foreach ($relationFields as $relationField)
{
if (isset($item[$relationField]))
{
$result[$relationField][] = $item[$relationField];
}
}
}
/**
* @param Throwable $throwable
*/
protected function runProcessingThrowable(Throwable $throwable): void
{
if (!is_subclass_of($throwable, RestException::class))
{
$throwable = new InternalException($throwable);
}
parent::runProcessingThrowable($throwable);
}
protected function writeToLogException(\Throwable $e): void
{
if ($e instanceof SkipWriteToLogException)
{
return;
}
if ($e instanceof InternalException && $e->getPrevious())
{
// get exception with real internal message
$e = $e->getPrevious();
}
parent::writeToLogException($e);
}
protected function buildErrorFromException(\Exception $e)
{
if ($e instanceof RestException)
{
$output = $e->output($this->localErrorLanguage);
return new Error($e->getMessage(), $e->getStatus(), !empty($output) ? $output : null);
}
return parent::buildErrorFromException($e);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\ObjectPropertyException;
use Bitrix\Main\SystemException;
use Bitrix\Rest\V3\Interaction\Request\TailRequest;
use Bitrix\Rest\V3\Interaction\Response\ListResponse;
interface TailActionInterface
{
/**
* @throws ObjectPropertyException
* @throws SystemException
* @throws ArgumentException
*/
public function tailAction(TailRequest $request): ListResponse;
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Main\DB\Order;
use Bitrix\Rest\V3\Exceptions\InvalidFilterException;
use Bitrix\Rest\V3\Interaction\Request\TailRequest;
use Bitrix\Rest\V3\Interaction\Response\ListResponse;
use Bitrix\Rest\V3\Structures\Filtering\Condition;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
use Bitrix\Rest\V3\Structures\Filtering\Operator;
use Bitrix\Rest\V3\Structures\Ordering\OrderStructure;
use Bitrix\Rest\V3\Structures\PaginationStructure;
trait TailOrmActionTrait
{
use OrmActionTrait;
final public function tailAction(TailRequest $request): ListResponse
{
if ($request->filter && in_array($request->cursor->getField(), $request->filter->getFields(), true))
{
throw new InvalidFilterException('Cursor field ' . $request->cursor->getField() . ' cannot be used at filter.');
}
if (!$request->filter)
{
$request->filter = new FilterStructure();
}
$field = 'id';
$order = Order::Asc->value;
$value = 0;
$limit = PaginationStructure::DEFAULT_LIMIT;
if ($request->cursor)
{
$field = $request->cursor->getField();
$order = $request->cursor->getOrder()->value;
$value = $request->cursor->getValue();
$limit = $request->cursor->getLimit();
}
$operator = $order === Order::Asc->value ? Operator::Greater : Operator::Less;
if ($request->cursor)
{
$condition = new Condition($field, $operator, $value);
$request->filter->addCondition($condition);
}
$orderStructure = OrderStructure::create(
[$field => $order],
$request->getDtoClass(),
$request
);
$paginationStructure = PaginationStructure::create(['limit' => $limit]);
$collection = $this
->getOrmRepositoryByRequest($request)
->getAll(
$request->select,
$request->filter,
$orderStructure,
$paginationStructure
);
return new ListResponse($collection);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Interaction\Request\UpdateRequest;
use Bitrix\Rest\V3\Interaction\Response\UpdateResponse;
interface UpdateActionInterface
{
public function updateAction(UpdateRequest $request): UpdateResponse;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Rest\V3\Exceptions\Validation\RequiredFieldInRequestException;
use Bitrix\Rest\V3\Interaction\Request\UpdateRequest;
use Bitrix\Rest\V3\Interaction\Response\UpdateResponse;
trait UpdateOrmActionTrait
{
use OrmActionTrait;
use ValidateDtoTrait;
public function updateAction(UpdateRequest $request): UpdateResponse
{
if ($request->id === null && $request->filter === null)
{
throw new RequiredFieldInRequestException('id || filter');
}
$repository = $this->getOrmRepositoryByRequest($request);
$result = $request->id !== null ?
$repository->update($request->id, $request->fields->getAsDto()) :
$repository->updateMulti($request->filter, $request->fields->getAsDto());
return new UpdateResponse($result);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Bitrix\Rest\V3\Controllers;
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Rest\V3\Dto\Dto;
trait ValidateDtoTrait
{
protected function validateDto(Dto $dto): bool
{
$validation = ServiceLocator::getInstance()->get('main.validation.service');
$result = $validation->validate($dto);
if ($result->isSuccess())
{
return true;
}
else
{
$this->addErrors($result->getErrors());
return false;
}
}
}

View File

@@ -0,0 +1,509 @@
<?php
namespace Bitrix\Rest\V3\Data;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\DB\Order;
use Bitrix\Main\ObjectPropertyException;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Main\ORM\Objectify\Collection;
use Bitrix\Main\ORM\Objectify\EntityObject;
use Bitrix\Main\ORM\Query\Filter\ConditionTree;
use Bitrix\Main\ORM\Query\Query;
use Bitrix\Main\SystemException;
use Bitrix\Main\Text\StringHelper;
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\Web\Json;
use Bitrix\Rest\V3\Attributes\JsonArray;
use Bitrix\Rest\V3\Attributes\OrmEntity;
use Bitrix\Rest\V3\Dto\Dto;
use Bitrix\Rest\V3\Dto\DtoCollection;
use Bitrix\Rest\V3\Dto\PropertyHelper;
use Bitrix\Rest\V3\Exceptions\ClassRequireAttributeException;
use Bitrix\Rest\V3\Exceptions\Internal\OrmSaveException;
use Bitrix\Rest\V3\Exceptions\InvalidPaginationException;
use Bitrix\Rest\V3\Exceptions\InvalidSelectException;
use Bitrix\Rest\V3\Exceptions\TooManyAttributesException;
use Bitrix\Rest\V3\Exceptions\UnknownDtoPropertyException;
use Bitrix\Rest\V3\Interaction\Request\ListRequest;
use Bitrix\Rest\V3\Structures\Aggregation\AggregationResultStructure;
use Bitrix\Rest\V3\Structures\Aggregation\AggregationSelectStructure;
use Bitrix\Rest\V3\Structures\Aggregation\AggregationType;
use Bitrix\Rest\V3\Structures\Aggregation\ResultItem;
use Bitrix\Rest\V3\Structures\Filtering\Condition;
use Bitrix\Rest\V3\Structures\Filtering\Expressions\ColumnExpression;
use Bitrix\Rest\V3\Structures\Filtering\Expressions\Expression;
use Bitrix\Rest\V3\Structures\Filtering\Expressions\LengthExpression;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
use Bitrix\Rest\V3\Structures\Ordering\OrderItem;
use Bitrix\Rest\V3\Structures\Ordering\OrderStructure;
use Bitrix\Rest\V3\Structures\PaginationStructure;
use Bitrix\Rest\V3\Structures\SelectStructure;
use Exception;
use ReflectionClass;
use ReflectionException;
class OrmRepository extends Repository
{
protected string $dataClass;
/**
* @param string $dtoClass
* @throws ReflectionException
*/
public function __construct(protected string $dtoClass)
{
$attributes = (new ReflectionClass($this->dtoClass))
->getAttributes(OrmEntity::class);
$attributesCount = count($attributes);
if ($attributesCount > 1)
{
throw new TooManyAttributesException($this->dtoClass, OrmEntity::class, 1);
}
else if ($attributesCount < 1)
{
throw new ClassRequireAttributeException($this->dtoClass, OrmEntity::class);
}
$this->dataClass = $attributes[0]->newInstance()->entity;
}
/**
* @throws SystemException
* @throws ArgumentException
*/
public function getAllWithAggregate(AggregationSelectStructure $select, ?FilterStructure $filter = null): AggregationResultStructure
{
$queryMap = [];
/** @var DataManager $dataClass */
$dataClass = $this->dataClass;
$query = $dataClass::query();
foreach ($select as $function)
{
$queryMap[$function->aggregation->value][$function->field] = $function->alias;
$aggregateFunction = self::mapAggregateFunction($function->aggregation->value);
$aggregateParam = self::mapDtoPropertyToOrmField($function->field);
$query->addSelect(Query::expr()->{$aggregateFunction}($aggregateParam), $function->alias);
}
if ($filter !== null)
{
$ormFilter = $this->prepareFilter($filter);
if ($ormFilter !== null)
{
$query->where($ormFilter);
}
}
$queryResult = $query->fetch();
$aggregationResult = new AggregationResultStructure();
foreach ($queryMap as $aggregation => $fields)
{
foreach ($fields as $field => $alias)
{
$aggregationType = AggregationType::from($aggregation);
$aggregateItem = new ResultItem($aggregationType, $field, $queryResult[$alias]);
$aggregationResult->add($aggregateItem);
}
}
return $aggregationResult;
}
/**
* @throws ArgumentException
* @throws SystemException
* @throws ObjectPropertyException
*/
public function getAll(
?SelectStructure $select = null,
?FilterStructure $filter = null,
?OrderStructure $order = null,
?PaginationStructure $page = null,
): DtoCollection {
$query = $this->getQuery($select, $filter, $order, $page);
return $this->mapCollectionToDto($query->fetchCollection());
}
/**
* @param SelectStructure|null $select
* @param FilterStructure|null $filter
* @param OrderStructure|null $order
* @param PaginationStructure|null $page
* @return Query
* @throws ArgumentException
* @throws SystemException
*/
public function getQuery(?SelectStructure $select, ?FilterStructure $filter = null, ?OrderStructure $order = null, ?PaginationStructure $page = null): Query
{
/** @var DataManager $dataClass */
$dataClass = $this->dataClass;
/** @var Collection $collection */
$query = $dataClass::query();
$query->setSelect($this->prepareSelect($select));
if ($filter !== null)
{
$ormFilter = $this->prepareFilter($filter);
if ($ormFilter !== null)
{
$query->where($ormFilter);
}
}
$query->setOrder($this->prepareOrder($order));
if ($page !== null)
{
$query
->setLimit($page->getLimit())
->setOffset($page->getOffset())
;
}
else
{
// hard limit
$query->setLimit(PaginationStructure::DEFAULT_LIMIT);
}
return $query;
}
/**
* @throws ArgumentException
* @throws Exception
*/
final protected function mapCollectionToDto(Collection $collection): DtoCollection
{
$dtoCollection = new DtoCollection($this->dtoClass);
foreach ($collection as $object)
{
$dtoCollection->add($this->mapObjectToDto($object, $this->dtoClass));
}
return $dtoCollection;
}
/**
* @throws ArgumentException
*/
protected function mapObjectToDto(EntityObject $object, string $dtoClass): Dto
{
$dto = new $dtoClass();
foreach ($object->collectValues() as $key => $value)
{
if (str_starts_with($key, 'UF_'))
{
$dto->{$key} = $value;
continue;
}
if (str_starts_with($key, 'UTS_'))
{
continue;
}
$dtoProperty = self::mapOrmFieldToDtoProperty($key);
if (property_exists($dto, $dtoProperty))
{
$dto->{$dtoProperty} = $value;
}
}
return $dto;
}
protected function prepareSelect(?SelectStructure $select): array
{
if ($select === null)
{
return ['*'];
}
$ormFields = [];
$dtoFields = $select->getList();
foreach ($dtoFields as $field)
{
$ormFields[] = self::mapDtoPropertyToOrmField($field);
}
foreach ($select->getUserFields() as $field)
{
$ormFields[] = $field;
}
if (!empty($select->getRelationFields()))
{
$ormEntityRelationFields = [];
foreach ($select->getRelationFields() as $field)
{
$ormEntityRelationFields[$field] = self::mapDtoPropertyToOrmField($field);
}
$ormFields = array_unique(array_merge($ormFields, $ormEntityRelationFields));
}
return $ormFields;
}
/**
* @param FilterStructure|null $filter
* @return ConditionTree|null
* @throws ArgumentException
* @throws SystemException
*/
protected function prepareFilter(?FilterStructure $filter = null): ?ConditionTree
{
if ($filter !== null && $filter->getConditions())
{
$query = new ConditionTree();
$query->logic($filter->logic()->value);
$query->negative($filter->isNegative());
foreach ($filter->getConditions() as $condition)
{
if ($condition instanceof Condition)
{
$query->where($this->convertFilterCondition($condition));
}
elseif ($condition instanceof FilterStructure)
{
$ormFilter = $this->prepareFilter($condition);
if ($ormFilter !== null)
{
$query->where($ormFilter);
}
}
}
return $query;
}
return null;
}
protected function prepareOrder(?OrderStructure $order = null): array
{
$orderItems = $order !== null ? $order->getItems() : [new OrderItem('id', Order::Asc)];
$ormOrder = [];
foreach ($orderItems as $item)
{
$ormField = self::mapDtoPropertyToOrmField($item->getProperty());
$ormOrder[$ormField] = $item->getOrder()->value;
}
return $ormOrder;
}
/**
* @throws SystemException
* @throws ArgumentException
*/
protected function convertFilterCondition(Condition $condition): \Bitrix\Main\ORM\Query\Filter\Condition
{
/** @var DataManager $dataClass */
$dataClass = $this->dataClass;
$leftOperand = $condition->getLeftOperand();
$rightOperand = $condition->getRightOperand();
$leftOperand = $leftOperand instanceof Expression ? $leftOperand : self::mapDtoPropertyToOrmField($leftOperand);
$operands = [&$leftOperand, &$rightOperand];
foreach ($operands as &$operand)
{
// columns
if ($operand instanceof ColumnExpression)
{
$operand = new \Bitrix\Main\ORM\Query\Filter\Expressions\ColumnExpression(
self::mapDtoPropertyToOrmField($operand->getProperty()),
);
}
// length expression
if ($operand instanceof LengthExpression)
{
$ormFieldName = self::mapDtoPropertyToOrmField($operand->getProperty());
$sqlHelper = $dataClass::getEntity()->getConnection()->getSqlHelper();
$operand = new ExpressionField(
\Bitrix\Main\ORM\Query\Expression::getTmpName('RST'),
$sqlHelper->getLengthFunction('%s'),
$ormFieldName,
);
}
}
return new \Bitrix\Main\ORM\Query\Filter\Condition(
$leftOperand,
$condition->getOperator()->value,
$rightOperand,
);
}
/**
* @throws OrmSaveException
* @throws ArgumentException
* @throws SystemException
*/
public function add(Dto $dto): int
{
/** @var DataManager $dataClass */
$dataClass = $this->dataClass;
/** @var EntityObject $ormObject */
$ormObject = $dataClass::createObject();
foreach ($dto->toArray(rawData: true) as $propertyName => $value)
{
if (str_starts_with($propertyName, 'UF_'))
{
$ormObject->set($propertyName, $value);
}
else
{
$ormFieldName = self::mapDtoPropertyToOrmField($propertyName);
$ormObject->set($ormFieldName, $value);
}
}
$result = $ormObject->save();
if ($result->isSuccess())
{
return $ormObject->getId();
}
else
{
$messages = implode(',', $result->getErrorMessages());
$internal = new Exception($messages);
throw new OrmSaveException($internal);
}
}
public function update(int $id, Dto $dto): bool
{
/** @var DataManager $dataClass */
$dataClass = $this->dataClass;
$ormFields = $this->getOrmFieldsByDto($dto);
return $dataClass::update($id, $ormFields)->getError() === null;
}
public function updateMulti(FilterStructure $filter, Dto $dto): bool
{
/** @var DataManager $dataClass */
$dataClass = $this->dataClass;
$ids = $this->getIdsByFilter($filter);
$ormFields = $this->getOrmFieldsByDto($dto);
return $dataClass::updateMulti($ids, $ormFields)->getError() === null;
}
private function getOrmFieldsByDto(Dto $dto): array
{
$ormFields = [];
foreach ($dto->toArray(rawData: true) as $propertyName => $value)
{
if (str_starts_with($propertyName, 'UF_'))
{
$ormFields[$propertyName] = $value;
}
else
{
$ormFields[self::mapDtoPropertyToOrmField($propertyName)] = $value;
}
}
return $ormFields;
}
public function delete(int $id): bool
{
/** @var DataManager $dataClass */
$dataClass = $this->dataClass;
$dataClass::delete($id);
return true;
}
public function deleteMulti(FilterStructure $filter): bool
{
$ids = $this->getIdsByFilter($filter);
foreach ($ids as $id)
{
$this->delete($id);
}
return true;
}
protected static function mapOrmFieldToDtoProperty(string $field): string
{
return StringHelper::snake2camel($field, true);
}
public static function mapDtoPropertyToOrmField(string $property): string
{
return strtoupper(StringHelper::camel2snake($property));
}
protected static function mapAggregateFunction(string $value): string
{
$availableMethods = ['sum', 'avg', 'max', 'min', 'count', 'countDistinct'];
if (in_array($value, $availableMethods, true))
{
return $value !== 'countDistinct' ? $value : 'CountDistinct';
}
throw new SystemException('Unsupported aggregation method: ' . $value . '. Use one of ' . join(', ', $availableMethods));
}
/**
* @throws UnknownDtoPropertyException
* @throws InvalidPaginationException
* @throws ArgumentException
* @throws InvalidSelectException
* @throws ObjectPropertyException
* @throws SystemException
*/
protected function getIdsByFilter(FilterStructure $filter): array
{
$query = $this->getQuery(
select: SelectStructure::create(['id'], $this->dtoClass, new ListRequest($this->dtoClass)),
filter: $filter,
page: PaginationStructure::create(['limit' => PaginationStructure::MAX_LIMIT]),
);
$rowsCursor = $query->exec();
$ids = [];
foreach ($rowsCursor as $row)
{
if (isset($row['ID']))
{
$ids[] = (int) $row['ID'];
}
}
return $ids;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Bitrix\Rest\V3\Data;
use Bitrix\Rest\V3\Dto\Dto;
use Bitrix\Rest\V3\Dto\DtoCollection;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
use Bitrix\Rest\V3\Structures\Ordering\OrderStructure;
use Bitrix\Rest\V3\Structures\PaginationStructure;
use Bitrix\Rest\V3\Structures\SelectStructure;
/**
* Repository for base actions get, list
*/
abstract class Repository
{
abstract public function getAll(
?SelectStructure $select = null,
?FilterStructure $filter = null,
?OrderStructure $order = null,
?PaginationStructure $page = null,
): DtoCollection;
final public function getOneWith(
?SelectStructure $select = null,
?FilterStructure $filter = null,
?OrderStructure $sort = null,
): ?Dto {
$page = PaginationStructure::create(['limit' => 1]);
$collection = $this->getAll($select, $filter, $sort, $page);
return $collection->first();
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Bitrix\Rest\V3\Documentation\Attributes;
use Bitrix\Rest\V3\Attributes\AbstractAttribute;
#[\Attribute]
class Deprecated extends AbstractAttribute
{
}

View File

@@ -0,0 +1,634 @@
<?php
namespace Bitrix\Rest\V3\Documentation;
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Type\Date;
use Bitrix\Main\Type\DateTime;
use Bitrix\Rest\V3\Attributes\Editable;
use Bitrix\Rest\V3\Attributes\ElementType;
use Bitrix\Rest\V3\Attributes\Sortable;
use Bitrix\Rest\V3\Documentation\Attributes\Deprecated;
use Bitrix\Rest\V3\Dto\Dto;
use Bitrix\Rest\V3\Dto\DtoCollection;
use Bitrix\Rest\V3\Dto\PropertyHelper;
use Bitrix\Rest\V3\Interaction\Request\AddRequest;
use Bitrix\Rest\V3\Interaction\Request\AggregateRequest;
use Bitrix\Rest\V3\Interaction\Request\DeleteRequest;
use Bitrix\Rest\V3\Interaction\Request\GetRequest;
use Bitrix\Rest\V3\Interaction\Request\ListRequest;
use Bitrix\Rest\V3\Interaction\Request\Request;
use Bitrix\Rest\V3\Interaction\Request\TailRequest;
use Bitrix\Rest\V3\Interaction\Request\UpdateRequest;
use Bitrix\Rest\V3\Interaction\Response\AddResponse;
use Bitrix\Rest\V3\Interaction\Response\AggregateResponse;
use Bitrix\Rest\V3\Interaction\Response\ArrayResponse;
use Bitrix\Rest\V3\Interaction\Response\BooleanResponse;
use Bitrix\Rest\V3\Interaction\Response\DeleteResponse;
use Bitrix\Rest\V3\Interaction\Response\GetResponse;
use Bitrix\Rest\V3\Interaction\Response\ListResponse;
use Bitrix\Rest\V3\Interaction\Response\Response;
use Bitrix\Rest\V3\Interaction\Response\UpdateResponse;
use Bitrix\Rest\V3\Schema\ControllerData;
use Bitrix\Rest\V3\Schema\ModuleManager;
use Bitrix\Rest\V3\Schema\SchemaManager;
use Bitrix\Rest\V3\Structures\Aggregation\AggregationType;
use CRestApiServer;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
class DocumentationManager
{
private SchemaManager $schemaManager;
private ModuleManager $moduleManager;
private array $reflections = [];
private const AVAILABLE_DEFAULT_RESPONSES = [
ArrayResponse::class,
GetResponse::class,
ListResponse::class,
AddResponse::class,
BooleanResponse::class,
DeleteResponse::class,
UpdateResponse::class,
AggregateResponse::class,
];
public function __construct()
{
$this->schemaManager = ServiceLocator::getInstance()->get(SchemaManager::class);
$this->moduleManager = ServiceLocator::getInstance()->get(ModuleManager::class);
}
/**
* @throws ReflectionException
*/
public function generateDataForJson(): array
{
$file = [
'openapi' => '3.0.0',
'info' => [
'title' => 'Bitrix24 REST V3 API',
],
'tags' => [],
'paths' => [],
'components' => ['schemas' => []],
];
$customModuleSchemas = $this->getCustomModuleSchemas();
$customModuleMethods = $this->getCustomModuleMethods();
$customModuleRoutes = $this->schemaManager->getRouteAliases();
$moduleControllers = $this->schemaManager->getControllersByModules();
foreach ($moduleControllers as $moduleId => $controllers)
{
$file['tags'][] = [
'name' => $moduleId,
'description' => $moduleId . ' module methods',
];
/** @var ControllerData $controllerData */
foreach ($controllers as $controllerData)
{
if ($controllerData->getDto() !== null)
{
$file['components']['schemas'][$controllerData->getDto()->getShortName()] =
$customModuleSchemas[$moduleId][$controllerData->getDto()->getShortName()]
?? $this->getDtoProperties($controllerData->getDto())
;
}
$this->addControllerMethodsToFile($controllerData, $customModuleMethods, $file);
}
}
foreach ($customModuleRoutes as $customRoute => $moduleRoute)
{
if (isset($file['paths'][$moduleRoute]))
{
$file['paths'][$customRoute] = $file['paths'][$moduleRoute];
}
}
return $file;
}
private function getCustomModuleSchemas(): array
{
$documentationSchemas = [];
$moduleConfigs = $this->moduleManager->getConfigs();
foreach ($moduleConfigs as $moduleId => $config)
{
if (!empty($config['documentation']['schemas']) && is_array($config['documentation']['schemas']))
{
foreach ($config['documentation']['schemas'] as $schemaObject => $schemaClass)
{
if (is_subclass_of($schemaClass, SchemaProvider::class))
{
$class = new $schemaClass();
$documentationSchemas[$moduleId][$schemaObject] = $class->getDocumentation();
}
}
}
}
return $documentationSchemas;
}
private function getCustomModuleMethods(): array
{
$documentationMethods = [];
$moduleConfigs = $this->moduleManager->getConfigs();
foreach ($moduleConfigs as $moduleId => $config)
{
if (!empty($config['documentation']['methods']) && is_array($config['documentation']['methods']))
{
foreach ($config['documentation']['methods'] as $methodUri => $methodDocumentationClass)
{
if (is_subclass_of($methodDocumentationClass, MethodProvider::class))
{
$class = new $methodDocumentationClass();
$documentationMethods[$moduleId][$methodUri] = $class->getDocumentation();
}
}
}
}
return $documentationMethods;
}
/**
* @throws ReflectionException
*/
private function addControllerMethodsToFile(ControllerData $controllerData, array $customModuleMethods, array &$file): void
{
$methods = $controllerData->getController()->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method)
{
if (str_ends_with($method->name, 'Action') && $method->getReturnType() instanceof ReflectionNamedType)
{
$returnType = $method->getReturnType()->getName();
if (!is_subclass_of($returnType, Response::class))
{
continue;
}
$methodName = substr($method->name, 0, -6); // cut Action
$methodUri = $controllerData->getMethodUri($methodName);
if (isset($customModuleMethods[$controllerData->getModuleId()][$methodUri]))
{
$methodData = $customModuleMethods[$controllerData->getModuleId()][$methodUri];
}
else
{
$methodData = $this->getMethodData($method, $returnType, $controllerData);
if ($methodData === null)
{
continue; // skip unknown return types
}
}
$file['paths'][$methodUri]['post'] = $methodData;
}
}
}
private function getResponseProperties(string $returnTypeClass, ?ReflectionClass $dtoReflection): array
{
$getResponseByClass = function (string $responseClass) use ($dtoReflection)
{
return match ($responseClass)
{
ArrayResponse::class => [
'type' => 'object',
],
GetResponse::class => [
'type' => 'object',
'properties' => $dtoReflection ? [
'item' => [
'$ref' => '#/components/schemas/' . $dtoReflection->getShortName(),
],
] : [],
],
ListResponse::class => [
'type' => 'array',
'items' => $dtoReflection ? [
'$ref' => '#/components/schemas/' . $dtoReflection->getShortName(),
] : [],
],
AddResponse::class => [
'type' => 'object',
'properties' => [
'id' => 'int64',
],
],
BooleanResponse::class, DeleteResponse::class, UpdateResponse::class => [
'type' => 'object',
'properties' => [
'result' => 'boolean',
],
],
AggregateResponse::class => [
'type' => 'object',
'properties' => [
'result' => [
'type' => 'object',
'properties' => $this->aggregateProperties(),
],
],
],
default => [],
};
};
if (in_array($returnTypeClass, self::AVAILABLE_DEFAULT_RESPONSES, true))
{
return $getResponseByClass($returnTypeClass);
}
foreach (self::AVAILABLE_DEFAULT_RESPONSES as $responseClass)
{
if (is_subclass_of($returnTypeClass, $responseClass))
{
return $getResponseByClass($responseClass);
}
}
return [];
}
private function aggregateProperties(): array
{
$aggregationProperties = [];
foreach (AggregationType::cases() as $aggregationType)
{
$aggregationProperties[$aggregationType->value] = [
'type' => 'object',
'properties' => [
'field' => 'string',
],
];
}
return $aggregationProperties;
}
private function getDtoProperty(ReflectionProperty $dtoProperty): array
{
if (!$dtoProperty->getType() instanceof ReflectionNamedType)
{
return [
'type' => 'unknown',
'format' => 'unknown',
];
}
$type = $dtoProperty->getType()->getName();
$types = [
'float' => ['type' => 'float'],
'array' => ['type' => 'array'],
'bool' => ['type' => 'boolean'],
'int' => ['type' => 'integer', 'format' => 'int64'],
'string' => ['type' => 'string'],
DateTime::class => ['type' => 'string', 'format' => 'date-time'],
Date::class => ['type' => 'string', 'format' => 'date'],
];
if (isset($types[$type]))
{
return $types[$type];
}
else if ($type === DtoCollection::class)
{
return $this->getDtoCollectionProperty($dtoProperty);
}
if (isset($this->reflections[$type]) && $this->reflections[$type]->isSubclassOf(Dto::class))
{
return ['$ref' => '#/components/schemas/' . $this->reflections[$type]->getShortName()];
}
else
{
return [
'type' => 'unknown',
'format' => 'unknown',
];
}
}
private function getDtoCollectionProperty(ReflectionProperty $dtoProperty): array
{
foreach ($dtoProperty->getAttributes(ElementType::class) as $propertyAttribute)
{
/** @var ElementType $instance */
$instance = $propertyAttribute->newInstance();
if (!isset($this->reflections[$instance->type]))
{
return [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
],
],
],
];
}
$dtoCollectionPropertyReflection = $this->reflections[$instance->type];
return [
'type' => 'array',
'items' => [
'$ref' => '#/components/schemas/' . $dtoCollectionPropertyReflection->getShortName(),
],
];
}
return [];
}
private function getDtoProperties(ReflectionClass $dtoReflection): array
{
$dtoProperties = $dtoReflection->getProperties(ReflectionProperty::IS_PUBLIC);
$result = [
'type' => 'object',
'properties' => [],
];
foreach ($dtoProperties as $dtoProperty)
{
$result['properties'][$dtoProperty->getName()] = $this->getDtoProperty($dtoProperty);
if (!$dtoProperty->getType()->allowsNull())
{
$result['required'][] = $dtoProperty->getName();
}
}
return $result;
}
private function getRequestTypeProperties(ReflectionParameter $parameter): array
{
if (!$parameter->getType() instanceof ReflectionNamedType)
{
return [
'type' => 'unknown',
];
}
$result = match($parameter->getType()->getName())
{
'int' => [
'type' => 'integer',
'example' => 1,
],
'string' => [
'type' => 'string',
'example' => 'string',
],
'float' => [
'type' => 'float',
'example' => 1.0,
],
'bool' => [
'type' => 'boolean',
'example' => true,
],
'array' => [
'type' => 'array',
],
};
if (!$parameter->getType()->allowsNull())
{
$result['required'] = true;
}
else
{
$result['nullable'] = true;
}
if ($parameter->isDefaultValueAvailable())
{
$result['example'] = $parameter->getDefaultValue();
}
return $result;
}
private function getRequestClassProperties(string $requestClass, ?ReflectionClass $dto): array
{
$examples = function (string $type, bool $allowNull = false) use ($requestClass, $dto)
{
$baseArray = match ($type)
{
'id' => [
'type' => 'integer',
'example' => 1,
],
'cursor' => [
'type' => 'object',
'example' => [
'field' => 'id',
'value' => 0,
'order' => 'ASC'
],
],
'filter' => [
'type' => 'array',
'example' => [['id', '>=', 1], ['id', 1], ['id', 'in', [1, 2, 3]]],
],
'select' => [
'type' => 'array',
'items' => [
'type' => 'string',
'example' => array_map(fn($property) => $property->getName(), PropertyHelper::getProperties($dto)),
],
],
'fields' => [
'type' => 'object',
'properties' => array_reduce(
PropertyHelper::getPropertiesWithAttribute($dto, Editable::class),
function ($data, $property)
{
$data[$property->getName()] = [
'type' => $property->getType()->getName(),
'format' => 'format',
'example' => 'example',
];
return $data;
},
[],
),
],
'order' => [
'type' => 'object',
'properties' => array_reduce(
PropertyHelper::getPropertiesWithAttribute($dto, Sortable::class),
function ($data, $property)
{
$data[$property->getName()] = [
'type' => 'string',
'example' => 'ASC',
];
return $data;
},
[],
),
],
'pagination' => [
'type' => 'object',
'properties' => [
'page' => ['type' => 'integer', 'example' => 2],
'limit' => ['type' => 'integer', 'example' => 20],
'offset' => ['type' => 'integer', 'example' => 0],
],
],
'aggregate' => [
'type' => 'object',
'properties' => [
'count' => ['type' => 'array', 'items' => ['type' => 'string'], 'example' => ['id']],
'min' => ['type' => 'array', 'items' => ['type' => 'string'], 'example' => ['id']],
'max' => ['type' => 'array', 'items' => ['type' => 'string'], 'example' => ['id']],
'avg' => ['type' => 'array', 'items' => ['type' => 'string'], 'example' => ['id']],
'sum' => ['type' => 'array', 'items' => ['type' => 'string'], 'example' => ['id']],
'countDistinct' => ['type' => 'array', 'items' => ['type' => 'string'], 'example' => ['id']],
],
],
default => [],
};
if ($allowNull)
{
$baseArray['nullable'] = true;
}
else
{
$baseArray['required'] = true;
}
return $baseArray;
};
return match ($requestClass)
{
GetRequest::class => [
'id' => $examples('id'),
'select' => $examples('select', true),
],
ListRequest::class => [
'select' => $examples('select', true),
'filter' => $examples('filter', true),
'order' => $examples('order', true),
'pagination' => $examples('pagination', true),
],
TailRequest::class => [
'select' => $examples('select', true),
'filter' => $examples('filter', true),
'cursor' => $examples('cursor', true),
],
AddRequest::class => [
'fields' => $examples('fields', true),
],
UpdateRequest::class => [
'id' => $examples('id', true),
'filter' => $examples('filter'),
'fields' => $examples('fields'),
],
DeleteRequest::class => [
'id' => $examples('id'),
'filter' => $examples('filter'),
],
AggregateRequest::class => [
'select' => $examples('aggregate'),
],
default => [],
};
}
/**
* @throws ReflectionException
*/
private function getMethodData(ReflectionMethod $method, string $returnTypeClass, ControllerData $controllerData): ?array
{
$properties = [];
$methodData = [];
$deprecated = !empty($method->getAttributes(Deprecated::class));
$parameters = $method->getParameters();
foreach ($parameters as $parameter)
{
if ($parameter->hasType())
{
if (!$parameter->getType()->isBuiltin())
{
$requestTypeReflection = new ReflectionClass($parameter->getType()->getName());
if ($requestTypeReflection->isSubclassOf(Request::class))
{
if ($controllerData->getDto() !== null)
{
array_push($properties, $this->getRequestClassProperties($parameter->getType()->getName(), $controllerData->getDto()));
}
}
else
{
return null; // skip unknown request types
}
}
else
{
$properties[$parameter->getName()] = $this->getRequestTypeProperties($parameter);
}
}
}
if ($deprecated)
{
$methodData['deprecated'] = true;
}
$methodData['tags'] = [$controllerData->getModuleId()];
$methodData['requestBody'] = [
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => $properties,
],
],
],
];
$methodData['responses'] = [
200 => [
'description' => 'Success response',
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'result' => $this->getResponseProperties($returnTypeClass, $controllerData->getDto()),
],
],
],
],
],
];
return $methodData;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Bitrix\Rest\V3\Documentation;
abstract class MethodProvider
{
protected const DEPRECATED = false;
abstract protected function getTags(): array;
abstract protected function getRequestBody(): array;
abstract protected function getResponses(): array;
public function getDocumentation(): array
{
$result = [
'tags' => $this->getTags(),
'requestBody' => $this->getRequestBody(),
'responses' => $this->getResponses(),
];
if (static::DEPRECATED)
{
$result['deprecated'] = true;
}
return $result;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Bitrix\Rest\V3\Documentation;
abstract class SchemaProvider
{
protected const DEFAULT_TYPE = 'object';
protected function getType(): string
{
return static::DEFAULT_TYPE;
}
abstract protected function getProperties(): array;
public function getDocumentation(): array
{
return [
'type' => $this->getType(),
'properties' => $this->getProperties(),
];
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Bitrix\Rest\V3\Dto;
use Bitrix\Main\ORM\Objectify\EntityObject;
use Bitrix\Main\Type\Contract\Arrayable;
use Bitrix\Main\Type\Date;
use Bitrix\Main\Type\DateTime;
use Bitrix\Rest\V3\Data\OrmRepository;
use Bitrix\Rest\V3\Exceptions\Internal\UnknownDtoPropertyInternalException;
use Bitrix\Rest\V3\Structures\UserFieldsTrait;
abstract class Dto implements Arrayable
{
use UserFieldsTrait;
public function __set(string $name, $value): void
{
if (str_starts_with($name, 'UF_'))
{
$this->userFields[$name] = $value;
return;
}
throw new UnknownDtoPropertyInternalException($name, static::class);
}
public function __get(string $name): mixed
{
if (str_starts_with($name, 'UF_'))
{
return $this->userFields[$name] ?? null;
}
return $this->$name;
}
public function toArray(bool $rawData = false): array
{
$values = [];
foreach (PropertyHelper::getProperties($this) as $property)
{
if ($property->isInitialized($this))
{
$values[$property->getName()] = $this->{$property->getName()};
}
}
foreach ($this->userFields as $key => $value)
{
$values[$key] = $value;
}
if ($rawData)
{
return $values;
}
foreach ($values as $propertyName => $value)
{
if ($value instanceof DateTime)
{
$values[$propertyName] = $value->format(DATE_ATOM);
}
elseif ($value instanceof Date)
{
$values[$propertyName] = $value->format('Y-m-d');
}
else
{
$values[$propertyName] = $value;
}
}
return $values;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Bitrix\Rest\V3\Dto;
use Bitrix\Main\SystemException;
use Bitrix\Main\Type\Contract\Arrayable;
class DtoCollection implements \IteratorAggregate, \Countable, Arrayable, \JsonSerializable
{
/** @var string Dto class name */
protected string $type;
protected array $items = [];
public function __construct(string $type)
{
if (!is_subclass_of($type, Dto::class))
{
throw new SystemException($type . ' is not instance of "' . Dto::class . '"');
}
$this->type = $type;
}
public function add(Dto $dto): void
{
$this->items[] = $dto;
}
public function getIterator(): \Traversable
{
return new \ArrayIterator($this->items);
}
public function getPropertyValues(string $propertyName): array
{
$values = [];
foreach ($this->items as $item)
{
if (isset($item->$propertyName))
{
$values[] = $item->$propertyName;
}
elseif (method_exists($item, 'get' . ucfirst($propertyName)))
{
$methodName = 'get' . ucfirst($propertyName);
$values[] = $item->$methodName();
}
elseif (method_exists($item, '__get'))
{
$values[] = $item->$propertyName;
}
}
return $values;
}
public function first(): ?Dto
{
return $this->items[0] ?? null;
}
public function toArray(): array
{
$result = [];
/** @var Dto $item */
foreach ($this->items as $item)
{
$result[] = $item->toArray();
}
return $result;
}
public function count(): int
{
return count($this->items);
}
public function jsonSerialize(): mixed
{
$result = [];
/** @var Dto $item */
foreach ($this->items as $item)
{
$result[] = $item->toArray();
}
return $result;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Bitrix\Rest\V3\Dto\Mapping;
use Bitrix\Rest\V3\Dto\Dto;
use Bitrix\Rest\V3\Dto\DtoCollection;
abstract class Mapper
{
final public function mapOne(mixed $item, array $fields = []): Dto
{
$collection = $this->mapCollection([$item], $fields);
return $collection->first();
}
abstract public function mapCollection(array $items, array $fields = []): DtoCollection;
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Bitrix\Rest\V3\Dto;
use Bitrix\Rest\V3\Attributes\AbstractAttribute;
use ReflectionClass;
final class PropertyHelper
{
/**
* @param string|ReflectionClass|Dto $dtoClass
* @return \ReflectionProperty[]
*/
public static function getProperties(string|ReflectionClass|Dto $dtoClass): array
{
$reflection = self::getReflection($dtoClass);
$properties = [];
foreach ($reflection->getProperties() as $property)
{
if ($property->isPublic() && !$property->isStatic())
{
$properties[] = $property;
}
}
return $properties;
}
public static function getProperty(string|ReflectionClass|Dto $dtoClass, string $propertyName): \ReflectionProperty|null
{
$reflection = self::getReflection($dtoClass);
if ($reflection->hasProperty($propertyName))
{
$property = $reflection->getProperty($propertyName);
if ($property->isPublic() && !$property->isStatic())
{
return $property;
}
}
return null;
}
public static function isValidProperty(string|ReflectionClass|Dto $dtoClass, $propertyName): bool
{
$reflection = self::getReflection($dtoClass);
$isValid = false;
if ($reflection->hasProperty($propertyName))
{
$property = $reflection->getProperty($propertyName);
if ($property->isPublic() && !$property->isStatic())
{
$isValid = true;
}
}
return $isValid;
}
/**
* @throws \ReflectionException
*/
public static function hasAttribute(string|ReflectionClass|Dto $dtoClass, string $propertyName, string $attributeName): bool
{
$reflection = self::getReflection($dtoClass);
$result = false;
$reflectionProperty = $reflection->getProperty($propertyName);
$attributes = $reflectionProperty->getAttributes();
foreach ($attributes as $attribute)
{
if ($attribute->getName() === $attributeName)
{
$result = true;
}
}
return $result;
}
public static function getAttribute(string|ReflectionClass|Dto $dtoClass, string $attributeName): ?AbstractAttribute
{
$reflection = self::getReflection($dtoClass);
$result = null;
$attributes = $reflection->getAttributes($attributeName);
if (!empty($attributes) && isset($attributes[0]))
{
$attributeInstance = $attributes[0]->newInstance();
if ($attributeInstance instanceof AbstractAttribute)
{
$result = $attributeInstance;
}
}
return $result;
}
public static function getPropertiesWithAttribute(string|ReflectionClass|Dto $dtoClass, string $attributeName): array
{
$reflection = self::getReflection($dtoClass);
$reflectionProperties = $reflection->getProperties();
$resultProperties = [];
foreach ($reflectionProperties as $reflectionProperty)
{
$attributes = $reflectionProperty->getAttributes();
foreach ($attributes as $attribute)
{
if ($attribute->getName() === $attributeName)
{
$resultProperties[] = $reflectionProperty;
}
}
}
return $resultProperties;
}
public static function getReflection(string|ReflectionClass|Dto $dtoClass): ReflectionClass
{
if ($dtoClass instanceof ReflectionClass)
{
return $dtoClass;
}
return new ReflectionClass($dtoClass);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class AccessDeniedException extends RestException
{
protected const STATUS = \CRestServer::STATUS_FORBIDDEN;
protected function getMessagePhraseCode(): string
{
return 'REST_ACCESS_DENIED';
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class ClassRequireAttributeException extends RestException
{
public function __construct(
public string $class,
public string $attribute,
) {
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_CLASS_REQUIRE_ATTRIBUTE_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#CLASS#' => (new \ReflectionClass($this->class))->getShortName(),
'#ATTRIBUTE#' => (new \ReflectionClass($this->attribute))->getShortName(),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class EntityNotFoundException extends RestException
{
public function __construct(
protected int $id,
) {
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_ENTITY_NOT_FOUND';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#ID#' => $this->id,
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
abstract class FieldException extends RestException
{
/**
* @param string $field
*/
public function __construct(
protected string $field,
) {
parent::__construct();
}
public function output($localErrorLanguage = null): array
{
$out = parent::output($localErrorLanguage);
$out['field'] = $this->field;
return $out;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Internal;
use Bitrix\Rest\V3\Exceptions\RestException;
use Throwable;
/**
* Showed to user as internal error without details.
* In turn, details are available in the original Exception passed through the constructor.
*/
class InternalException extends RestException
{
/**
* @param Throwable $original Real internal exception for debug
*/
public function __construct(Throwable $original, string $status = \CRestServer::STATUS_INTERNAL)
{
parent::__construct($original, $status);
}
protected function getMessagePhraseCode(): string
{
return 'REST_INTERNAL_EXCEPTION';
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Internal;
class OrmSaveException extends InternalException
{
protected function getMessagePhraseCode(): string
{
return 'REST_INTERNAL_ORM_SAVE_EXCEPTION';
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Internal;
class UnknownDtoPropertyInternalException extends InternalException
{
public function __construct(string $propertyName, string $dtoClass)
{
$this->message = "Property `{$propertyName}` not found in `{$dtoClass}`";
parent::__construct($this);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class InvalidClassInstanceProvidedException extends RestException
{
public function __construct(
public string $provided,
public string $required,
) {
return parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_INVALID_CLASS_INSTANCE_PROVIDED_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#PROVIDED#' => (new \ReflectionClass($this->provided))->getShortName(),
'#REQUIRED#' => (new \ReflectionClass($this->required))->getShortName(),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class InvalidFilterException extends RestException
{
public function __construct(
protected mixed $filter,
) {
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_INVALID_FILTER_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#FILTER#' => is_string($this->filter) ? $this->filter : json_encode($this->filter),
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class InvalidJsonException extends RestException
{
protected function getMessagePhraseCode(): string
{
return 'REST_INVALID_JSON_EXCEPTION';
}
protected function getClassWithPhrase(): string
{
return self::class;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class InvalidOrderException extends RestException
{
public function __construct(
protected mixed $order,
) {
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_INVALID_ORDER_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#ORDER#' => is_string($this->order) ? $this->order : json_encode($this->order),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class InvalidPaginationException extends RestException
{
public function __construct(
protected mixed $page,
) {
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_INVALID_PAGINATION_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#PAGE#' => is_string($this->page) ? $this->page : json_encode($this->page),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class InvalidSelectException extends RestException
{
public function __construct(
protected mixed $select,
) {
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_INVALID_SELECT_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#SELECT#' => is_string($this->select) ? $this->select : json_encode($this->select),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class LicenseException extends RestException
{
protected function getMessagePhraseCode(): string
{
return 'REST_LICENSE_EXCEPTION';
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class LogicException extends RestException
{
protected function getMessagePhraseCode(): string
{
return 'REST_LOGIC_EXCEPTION';
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
use CRestServer;
class MethodNotFoundException extends RestException
{
protected const STATUS = CRestServer::STATUS_NOT_FOUND;
public function __construct(protected string $method)
{
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_METHOD_NOT_FOUND_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#METHOD#' => $this->method,
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class RateLimitException extends RestException
{
protected const STATUS = \CRestServer::STATUS_TO_MANY_REQUESTS;
protected function getMessagePhraseCode(): string
{
return 'REST_RATE_LIMIT_EXCEPTION';
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\SystemException;
use Bitrix\Rest\RestExceptionInterface;
abstract class RestException extends SystemException implements RestExceptionInterface
{
protected const STATUS = '400 Bad Request';
protected string $status;
public function __construct(\Throwable $previous = null, ?string $status = null)
{
$this->message = $this->getLocalMessage('en');
$this->status = $status === null ? static::STATUS : $status;
parent::__construct(message: $this->message, previous: $previous);
}
public function getRegistryCode(): string
{
$code = $this->getClassWithPhrase();
$code = str_replace('\\', '_', $code);
return strtoupper($code);
}
protected function getGlobalMessage(): string
{
return $this->message;
}
protected function getLocalMessage(string $languageCode): string
{
// include lang file
$reflection = new \ReflectionClass($this->getClassWithPhrase());
Loc::loadLanguageFile($reflection->getFileName(), $languageCode);
// return final phrase
return Loc::getMessage(
$this->getMessagePhraseCode(),
$this->getMessagePhraseReplacement(),
$languageCode,
);
}
public function output($localErrorLanguage = null): array
{
$out = [
'code' => $this->getRegistryCode(),
'message' => $this->getGlobalMessage(),
];
if (isset($localErrorLanguage))
{
$out['localMessage'] = $this->getLocalMessage($localErrorLanguage);
}
return $out;
}
abstract protected function getMessagePhraseCode(): string;
public function getStatus(): string
{
return $this->status;
}
protected function getClassWithPhrase(): string
{
return static::class;
}
protected function getMessagePhraseReplacement(): ?array
{
return null;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
interface SkipWriteToLogException
{
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class TooManyAttributesException extends RestException
{
public function __construct(
public string $class,
public string $attribute,
public int $expectedCount,
) {
return parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_TOO_MANY_ATTRIBUTES_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#CLASS#' => (new \ReflectionClass($this->class))->getShortName(),
'#ATTRIBUTE#' => (new \ReflectionClass($this->attribute))->getShortName(),
'#EXPECTED#' => $this->expectedCount,
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class UnknownAggregateFunctionException extends RestException
{
public function __construct(
protected string $function,
) {
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_UNKNOWN_AGGREGATE_FUNCTION_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#FUNCTION#' => $this->function,
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class UnknownDtoPropertyException extends RestException
{
public function __construct(
public string $dtoClass,
public string $propertyName,
) {
return parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_UNKNOWN_DTO_PROPERTY_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#DTO#' => (new \ReflectionClass($this->dtoClass))->getShortName(),
'#FIELD#' => $this->propertyName,
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Bitrix\Rest\V3\Exceptions;
class UnknownFilterOperatorException extends RestException
{
public function __construct(
protected string $operator,
) {
parent::__construct();
}
protected function getMessagePhraseCode(): string
{
return 'REST_UNKNOWN_FILTER_OPERATOR_EXCEPTION';
}
protected function getMessagePhraseReplacement(): ?array
{
return [
'#OPERATOR#' => $this->operator,
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Validation;
use Bitrix\Main\Error;
use Bitrix\Main\Localization\LocalizableMessage;
class DtoFieldRequiredAttributeException extends RequestValidationException
{
public function __construct(string $dto, string $field, string $attribute)
{
$message = new LocalizableMessage(
'REST_DTO_FIELD_REQUIRE_ATTRIBUTE_EXCEPTION', [
'#FIELD#' => $field,
'#DTO#' => (new \ReflectionClass($dto))->getShortName(),
'#ATTRIBUTE#' => (new \ReflectionClass($attribute))->getShortName(),
],
);
parent::__construct([new Error($message, $field)]);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Validation;
class DtoValidationException extends ValidationException
{
protected function getMessagePhraseCode(): string
{
return 'REST_DTO_VALIDATION_EXCEPTION';
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Validation;
use Bitrix\Main\Error;
use Bitrix\Main\Localization\LocalizableMessage;
class InvalidRequestFieldTypeException extends RequestValidationException
{
public function __construct(string $field, string $type)
{
$message = new LocalizableMessage(
'REST_INVALID_REQUEST_FIELD_TYPE_EXCEPTION', [
'#FIELD#' => $field,
'#TYPE#' => class_exists($type) ? (new \ReflectionClass($type))->getShortName() : $type,
],
);
parent::__construct([new Error($message, $field)]);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Validation;
abstract class RequestValidationException extends ValidationException
{
protected function getMessagePhraseCode(): string
{
return 'REST_REQUEST_VALIDATION_EXCEPTION';
}
protected function getClassWithPhrase(): string
{
return self::class;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Validation;
use Bitrix\Main\Error;
use Bitrix\Main\Localization\LocalizableMessage;
class RequiredFieldInRequestException extends RequestValidationException
{
public function __construct(string $field)
{
$message = new LocalizableMessage(
'REST_REQUIRED_FIELD_IN_REQUEST_EXCEPTION', ['#FIELD#' => $field],
);
parent::__construct([new Error($message, $field)]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Bitrix\Rest\V3\Exceptions\Validation;
use Bitrix\Main\Error;
use Bitrix\Rest\V3\Exceptions\RestException;
use Bitrix\Rest\V3\Exceptions\SkipWriteToLogException;
/**
* This class is used for displaying validation errors.
* It supports outputting multiple error messages linked to specific fields.
*/
abstract class ValidationException extends RestException implements SkipWriteToLogException
{
/**
* @param Error[] $errors
*/
public function __construct(
protected array $errors,
) {
parent::__construct();
}
public function output($localErrorLanguage = null): array
{
$out = parent::output($localErrorLanguage);
$validationItems = [];
foreach ($this->errors as $error)
{
$validationItem = [
'message' => $error->getLocalizableMessage()?->localize('en') ?? $error->getMessage(),
];
if (isset($localErrorLanguage))
{
$validationItem['localMessage'] = $error->getLocalizableMessage()?->localize($localErrorLanguage)
?? $error->getMessage();
}
if (!empty($error->getCode()))
{
$validationItem['field'] = $error->getCode();
}
$validationItems[] = $validationItem;
}
$out['validation'] = $validationItems;
return $out;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Bitrix\Rest\V3\Interaction;
use Bitrix\Rest\V3\Interaction\Request\Request;
use Bitrix\Rest\V3\Interaction\Response\ResponseWithRelations;
class Relation
{
private ?ResponseWithRelations $response = null;
public function __construct(
private string $name,
private string $method,
private string $fromField,
private string $toField,
private Request $request,
private bool $multiply,
) {
}
public function getName(): string
{
return $this->name;
}
public function getMethod(): string
{
return $this->method;
}
public function getFromField(): string
{
return $this->fromField;
}
public function getToField(): string
{
return $this->toField;
}
public function getRequest(): Request
{
return $this->request;
}
public function isMultiply(): bool
{
return $this->multiply;
}
public function getResponse(): ?ResponseWithRelations
{
return $this->response;
}
public function setResponse(ResponseWithRelations $response): self
{
$this->response = $response;
return $this;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Structures\FieldsStructure;
class AddRequest extends Request
{
public FieldsStructure $fields;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Structures\Aggregation\AggregationSelectStructure;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
class AggregateRequest extends Request
{
public AggregationSelectStructure $select;
public ?FilterStructure $filter = null;
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Exceptions\InvalidSelectException;
class BatchRequest
{
/**
* @var BatchRequestItem[]
*/
private array $items = [];
private array $itemsByAlias = [];
public function __construct(array $data)
{
foreach ($data as $item)
{
if (!isset($item['method']) || !isset($item['query']))
{
throw new InvalidSelectException('Each request item must have a "method" and "query" attribute');
}
$batchRequest = new BatchRequestItem($item['method'], $item['query'], $item['as'] ?? null, $item['parallel'] ?? false);
$this->items[] = $batchRequest;
if ($batchRequest->getAlias())
{
if (isset($this->itemsByAlias[$batchRequest->getAlias()]))
{
throw new InvalidSelectException('You are can not have two aliases with same name: ' . $batchRequest->getAlias());
}
$this->itemsByAlias[$batchRequest->getAlias()] = $batchRequest;
}
}
}
public function getItems(): array
{
return $this->items;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Interaction\Response\Response;
class BatchRequestItem
{
private Response $response;
public function __construct(private string $method, private array $query, private ?string $alias = null, private bool $parallel = false)
{
}
public function getMethod(): string
{
return $this->method;
}
public function getQuery(): array
{
return $this->query;
}
public function getAlias(): ?string
{
return $this->alias;
}
public function isParallel(): bool
{
return $this->parallel;
}
public function getResponse(): Response
{
return $this->response;
}
public function setResponse(Response $response): BatchRequestItem
{
$this->response = $response;
return $this;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
class DeleteRequest extends Request
{
public ?int $id = null;
public ?FilterStructure $filter = null;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Structures\SelectStructure;
class GetRequest extends Request
{
public int $id;
public ?SelectStructure $select = null;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
use Bitrix\Rest\V3\Structures\Ordering\OrderStructure;
use Bitrix\Rest\V3\Structures\SelectStructure;
use Bitrix\Rest\V3\Structures\PaginationStructure;
class ListRequest extends Request
{
public ?SelectStructure $select = null;
public ?FilterStructure $filter = null;
public ?OrderStructure $order = null;
public ?PaginationStructure $pagination = null;
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\HttpRequest;
use Bitrix\Main\SystemException;
use Bitrix\Rest\V3\Attributes\OrmEntity;
use Bitrix\Rest\V3\Dto\Dto;
use Bitrix\Rest\V3\Exceptions\InvalidJsonException;
use Bitrix\Rest\V3\Exceptions\Validation\RequiredFieldInRequestException;
use Bitrix\Rest\V3\Interaction\Relation;
use Bitrix\Rest\V3\Structures\Structure;
use ReflectionClass;
use ReflectionNamedType;
abstract class Request
{
protected ?string $ormEntityClass = null;
/**
* @var Relation[]
*/
protected array $relations = [];
public function __construct(protected string $dtoClass)
{
}
public function getRelations(): array
{
return $this->relations;
}
public function getRelation(string $relationName): ?Relation
{
return $this->relations[$relationName] ?? null;
}
public function addRelation(Relation $relation): void
{
$this->relations[$relation->getName()] = $relation;
}
/**
* @param HttpRequest $httpRequest
* @param string $dtoClass
* @return Request
* @throws InvalidJsonException
* @throws RequiredFieldInRequestException
* @throws SystemException
* @see Dto
*/
public static function create(HttpRequest $httpRequest, string $dtoClass): self
{
$request = new static($dtoClass);
// input data
try
{
$httpRequest->decodeJsonStrict();
$input = $httpRequest->getJsonList();
}
catch (ArgumentException)
{
throw new InvalidJsonException();
}
// properties of request
$reflection = new ReflectionClass($request);
$properties = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC);
// set input data into the request
foreach ($properties as $property)
{
if (!$property->getType() instanceof ReflectionNamedType)
{
continue;
}
$propertyName = $property->getName();
$propertyType = $property->getType()->getName();
$isOptional = $property->getType()->allowsNull();
if (!isset($input[$propertyName]))
{
if (!$isOptional)
{
// field not found, but it is required
throw new RequiredFieldInRequestException($propertyName);
}
continue;
}
if (is_subclass_of($propertyType, Structure::class))
{
// validate with Dto
$value = $propertyType::create($input[$propertyName], $dtoClass, $request);
}
else
{
$value = $input[$propertyName];
}
$request->{$propertyName} = $value;
}
return $request;
}
public function getDtoClass(): string
{
return $this->dtoClass;
}
public function getOrmEntityClass(): ?string
{
return $this->ormEntityClass;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Main\HttpRequest;
final class ServerRequest
{
protected ?string $scope = null;
protected ?string $token = null;
public function __construct(private string $method, private array $query = [], private HttpRequest $httpRequest)
{
if (isset($this->query['token']) && !empty($this->query['token']))
{
$this->token = $this->query['token'];
}
}
public function getMethod(): string
{
return $this->method;
}
public function getQuery(): array
{
return $this->query;
}
public function setQuery(array $query): static
{
$this->query = $query;
return $this;
}
public function getScope(): ?string
{
return $this->scope;
}
public function setScope(?string $scope): self
{
$this->scope = $scope;
return $this;
}
public function getToken(): ?string
{
return $this->token;
}
public function getHttpRequest(): HttpRequest
{
return $this->httpRequest;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Structures\CursorStructure;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
use Bitrix\Rest\V3\Structures\SelectStructure;
class TailRequest extends Request
{
public ?SelectStructure $select = null;
public ?FilterStructure $filter = null;
public ?CursorStructure $cursor = null;
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Request;
use Bitrix\Rest\V3\Structures\FieldsStructure;
use Bitrix\Rest\V3\Structures\Filtering\FilterStructure;
class UpdateRequest extends Request
{
public ?int $id = null;
public FieldsStructure $fields;
public ?FilterStructure $filter = null;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
class AddResponse extends Response
{
/**
* @param int $id
*/
public function __construct(public int $id)
{
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
use Bitrix\Rest\V3\Structures\Aggregation\AggregationResultStructure;
class AggregateResponse extends Response
{
/**
* @param AggregationResultStructure $result
*/
public function __construct(public AggregationResultStructure $result)
{
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
use Bitrix\Main\Type\Contract\Arrayable;
class ArrayResponse extends Response
{
public function __construct(protected ?array $array = [])
{
}
public function toArray(): array
{
$result = [];
foreach ($this->array as $key => $value)
{
if ($value instanceof Arrayable)
{
$result[$key] = $value->toArray();
}
else
{
$result[$key] = $value;
}
}
return $result;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
class BatchResponse extends Response
{
/**
* @var Response[]
*/
private array $items = [];
private array $context = [];
public function addItem(int|string $alias, Response $item): void
{
$responseData = $item->toArray();
if ($responseData['item'])
{
$this->context[$alias] = $responseData['item'];
}
else if ($responseData['items'])
{
$this->context[$alias] = $responseData['items'];
}
$this->items[] = $item;
}
public function toArray(): array
{
$result = [];
foreach ($this->items as $item)
{
$result[] = $item->toArray();
}
return $result;
}
public function getContext(int|string|null $alias = null): array
{
return ($alias !== null && isset($this->context[$alias])) ? $this->context[$alias] : $this->context;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
class BooleanResponse extends Response
{
/**
* @param bool $result
*/
public function __construct(public bool $result = true)
{
return $this;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
class DeleteResponse extends BooleanResponse
{
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
use Bitrix\Main\Error;
use Bitrix\Rest\RestExceptionInterface;
class ErrorResponse extends ArrayResponse implements RestExceptionInterface
{
protected bool $showDebugInfo = false;
protected bool $showRawData = true;
protected string $status = \CRestServer::STATUS_WRONG_REQUEST;
protected int $code;
/**
* @param Error[] $errors
*/
public function __construct(array $errors)
{
$error = $errors[0];
$this->status = $error->getCode();
$this->code = (int) $error->getCode();
$result = ['error' => $this->getSingleErrorResponseData($error)];
parent::__construct($result);
}
protected function getSingleErrorResponseData(Error $error): array
{
$data = [
'code' => $error->getCode(),
'message' => $error->getMessage(),
];
if ($error->getCustomData())
{
$data = array_merge($data, $error->getCustomData());
}
return $data;
}
public function output(): array
{
return [];
}
public function getStatus(): string
{
return $this->status;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
use Bitrix\Rest\V3\Dto\Dto;
class GetResponse extends ResponseWithRelations
{
public array $item;
/**
* @param Dto $item
*/
public function __construct(Dto $item)
{
$this->item = $item->toArray();
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
use Bitrix\Rest\V3\Dto\DtoCollection;
class ListResponse extends ResponseWithRelations
{
public array $items;
public function __construct(DtoCollection $items)
{
$this->items = $items->toArray();
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
use Bitrix\Main\Type\Contract\Arrayable;
abstract class Response implements Arrayable
{
protected bool $showDebugInfo = true;
protected bool $showRawData = false;
public function toArray(): array
{
$reflection = new \ReflectionClass($this);
$properties = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC);
$result = [];
foreach ($properties as $property)
{
$result[$property->getName()] = $property->getValue($this) instanceof Arrayable ? $property->getValue($this)->toArray() : $property->getValue($this);
}
return $result;
}
public function isShowDebugInfo(): bool
{
return $this->showDebugInfo;
}
public function setShowDebugInfo(bool $showDebugInfo): self
{
$this->showDebugInfo = $showDebugInfo;
return $this;
}
public function isShowRawData(): bool
{
return $this->showRawData;
}
public function setShowRawData(bool $showRawData): self
{
$this->showRawData = $showRawData;
return $this;
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
use Bitrix\Main\Type\Contract\Arrayable;
use Bitrix\Rest\V3\Interaction\Relation;
use Bitrix\Rest\V3\Interaction\Request\Request;
abstract class ResponseWithRelations extends Response
{
protected ?Request $parentRequest = null;
/**
* @var Relation[]
*/
protected array $relations = [];
public function toArray(): array
{
$result = parent::toArray();
foreach ($this->getRelations() as $relation)
{
$relationResponse = $relation->getResponse();
if ($relationResponse === null)
{
continue;
}
$relationData = $relationResponse->toArray();
$relationName = $relation->getName();
$fromField = $relation->getFromField();
$toField = $relation->getToField();
$isFromFieldRequested = in_array($fromField, $relation->getRequest()->select->getList(), true);
$isToFieldRequested = in_array($toField, $relation->getRequest()->select->getList(), true);
if ($this instanceof ListResponse)
{
foreach ($result['items'] as &$item)
{
if (isset($item[$fromField]))
{
$this->mergeRelationData(
$item,
$relationName,
$relationData,
$relation->isMultiply(),
$fromField,
$toField,
$item[$fromField],
$isFromFieldRequested,
$isToFieldRequested
);
}
}
unset($item);
}
elseif ($this instanceof GetResponse)
{
if (isset($result['item'][$fromField]))
{
$this->mergeRelationData(
$result['item'],
$relationName,
$relationData,
$relation->isMultiply(),
$fromField,
$toField,
$result['item'][$fromField],
$isFromFieldRequested,
$isToFieldRequested
);
}
}
}
return $result;
}
public function getParentRequest(): ?Request
{
return $this->parentRequest;
}
public function setParentRequest(Request $parentRequest): self
{
$this->parentRequest = $parentRequest;
return $this;
}
/**
* @return Relation[]
*/
public function getRelations(): array
{
return $this->relations;
}
public function setRelations(array $relations): self
{
$this->relations = $relations;
return $this;
}
/**
* Объединяет данные relation с основными данными
*/
private function mergeRelationData(
array &$data,
string $relationName,
array $relationData,
bool $isMultiply,
string $fromField,
string $toField,
$currentValue,
bool $isFromFieldRequested,
bool $isToFieldRequested
): void {
$matchedItems = [];
if (isset($relationData['items']))
{
// ListResponse
foreach ($relationData['items'] as $relationItem)
{
if (isset($relationItem[$toField]) && $relationItem[$toField] === $currentValue)
{
if (!$isToFieldRequested)
{
unset($relationItem[$toField]);
}
$matchedItems[] = $relationItem;
}
}
} elseif (isset($relationData['item']))
{
// GetResponse
if (isset($relationData['item'][$toField]) && $relationData['item'][$toField] === $currentValue)
{
if (!$isToFieldRequested)
{
unset($relationData['item'][$toField]);
}
$matchedItems[] = $relationData['item'];
}
}
if (!empty($matchedItems))
{
$data[$relationName] = $isMultiply ? $matchedItems : $matchedItems[0];
}
else
{
$data[$relationName] = $isMultiply ? [] : null;
}
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bitrix\Rest\V3\Interaction\Response;
class UpdateResponse extends BooleanResponse
{
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Bitrix\Rest\V3\Realisation\Controllers;
use Bitrix\Main\Composite\Internals\Locker;
use Bitrix\Main\SystemException;
use Bitrix\Rest\V3\Attributes\Scope;
use Bitrix\Rest\V3\Controllers\RestController;
use Bitrix\Rest\V3\Documentation\DocumentationManager;
use Bitrix\Rest\V3\Interaction\Response\ArrayResponse;
use Bitrix\Rest\V3\CacheManager;
class Documentation extends RestController
{
private const DOCUMENTATION_CACHE_KEY = 'rest.v3.documentation.cache.key';
#[Scope(\CRestUtil::GLOBAL_SCOPE)]
public function openApiAction(): ArrayResponse
{
if (!Locker::lock(self::DOCUMENTATION_CACHE_KEY))
{
throw new SystemException('Generation in progress.');
}
$result = CacheManager::get(self::DOCUMENTATION_CACHE_KEY);
if ($result === null)
{
$manager = new DocumentationManager();
$result = $manager->generateDataForJson();
CacheManager::set(self::DOCUMENTATION_CACHE_KEY, $result);
}
return (new ArrayResponse($result))->setShowDebugInfo(false)->setShowRawData(true);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Bitrix\Rest\V3\Schema;
class ControllerData
{
private \ReflectionClass $controller;
private ?\ReflectionClass $dto = null;
public function __construct(
private readonly string $moduleId,
private readonly string $controllerClass,
private readonly ?string $dtoClass = null,
private readonly ?string $namespace = null,
) {
$this->controller = new \ReflectionClass($this->controllerClass);
if ($this->dtoClass)
{
$this->dto = new \ReflectionClass($this->dtoClass);
}
}
public function getUri(): string
{
$namespace = strtolower(trim($this->namespace, '\\'));
$controllerName = strtolower($this->controller->getName());
$controllerUri = str_replace('\\', '.', trim(str_replace($namespace,'', $controllerName), '\\'));
return $this->moduleId . '.' . $controllerUri;
}
public function getMethodUri(string $method): string
{
return $this->getUri() . '.' . strtolower($method);
}
public function getModuleId(): string
{
return $this->moduleId;
}
public function getController(): \ReflectionClass
{
return $this->controller;
}
public function getDto(): ?\ReflectionClass
{
return $this->dto;
}
public function getNamespace(): ?string
{
return $this->namespace;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Bitrix\Rest\V3\Schema;
class MethodDescription
{
public function __construct(
private readonly string $method,
private readonly ?string $controller,
private readonly string $scope,
private readonly string $module,
private readonly ?string $class = null,
)
{
}
public function getMethod(): string
{
return $this->method;
}
public function getController(): ?string
{
return $this->controller;
}
public function getScope(): string
{
return $this->scope;
}
public function getModule(): string
{
return $this->module;
}
public function getClass(): ?string
{
return $this->class;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Bitrix\Rest\V3\Schema;
use Bitrix\Main\Config\Configuration;
use Bitrix\Main\ModuleManager as MainModuleManager;
use Bitrix\Rest\V3\CacheManager;
final class ModuleManager
{
private const CONFIGS_CACHE_KEY = 'rest.v3.ModuleManager.module.configs.cache.key';
private const CONFIGURATION_KEY = 'rest';
public function getConfigs(): array
{
$configs = CacheManager::get(self::CONFIGS_CACHE_KEY);
if ($configs === null)
{
foreach (MainModuleManager::getInstalledModules() as $moduleId => $moduleData)
{
$config = Configuration::getInstance($moduleId)->get(self::CONFIGURATION_KEY);
if ($config !== null)
{
$configs[$moduleId] = $config;
}
}
CacheManager::set(self::CONFIGS_CACHE_KEY, $configs);
}
return $configs;
}
}

Some files were not shown because too many files have changed in this diff Show More