Update
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
abstract class AbstractAttribute
|
||||
{
|
||||
|
||||
}
|
||||
11
core/bitrix/modules/rest/lib/V3/Attributes/DtoType.php
Normal file
11
core/bitrix/modules/rest/lib/V3/Attributes/DtoType.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
#[\Attribute]
|
||||
class DtoType extends AbstractAttribute
|
||||
{
|
||||
public function __construct(public readonly string $type)
|
||||
{
|
||||
}
|
||||
}
|
||||
9
core/bitrix/modules/rest/lib/V3/Attributes/Editable.php
Normal file
9
core/bitrix/modules/rest/lib/V3/Attributes/Editable.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
#[\Attribute]
|
||||
class Editable extends AbstractAttribute
|
||||
{
|
||||
|
||||
}
|
||||
11
core/bitrix/modules/rest/lib/V3/Attributes/ElementType.php
Normal file
11
core/bitrix/modules/rest/lib/V3/Attributes/ElementType.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
#[\Attribute]
|
||||
class ElementType extends AbstractAttribute
|
||||
{
|
||||
public function __construct(public readonly string $type)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
#[\Attribute]
|
||||
class Filterable extends AbstractAttribute
|
||||
{
|
||||
|
||||
}
|
||||
8
core/bitrix/modules/rest/lib/V3/Attributes/JsonArray.php
Normal file
8
core/bitrix/modules/rest/lib/V3/Attributes/JsonArray.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
#[\Attribute]
|
||||
class JsonArray extends AbstractAttribute
|
||||
{
|
||||
}
|
||||
8
core/bitrix/modules/rest/lib/V3/Attributes/Nullable.php
Normal file
8
core/bitrix/modules/rest/lib/V3/Attributes/Nullable.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Bitrix\Rest\V3\Attributes\AbstractAttribute;
|
||||
|
||||
#[\Attribute]
|
||||
class Nullable extends AbstractAttribute
|
||||
{
|
||||
}
|
||||
9
core/bitrix/modules/rest/lib/V3/Attributes/Optional.php
Normal file
9
core/bitrix/modules/rest/lib/V3/Attributes/Optional.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
#[\Attribute]
|
||||
class Optional extends AbstractAttribute
|
||||
{
|
||||
|
||||
}
|
||||
18
core/bitrix/modules/rest/lib/V3/Attributes/OrmEntity.php
Normal file
18
core/bitrix/modules/rest/lib/V3/Attributes/OrmEntity.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
16
core/bitrix/modules/rest/lib/V3/Attributes/RelationToOne.php
Normal file
16
core/bitrix/modules/rest/lib/V3/Attributes/RelationToOne.php
Normal 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
|
||||
) {}
|
||||
}
|
||||
19
core/bitrix/modules/rest/lib/V3/Attributes/ResolvedBy.php
Normal file
19
core/bitrix/modules/rest/lib/V3/Attributes/ResolvedBy.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
core/bitrix/modules/rest/lib/V3/Attributes/Scope.php
Normal file
11
core/bitrix/modules/rest/lib/V3/Attributes/Scope.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
#[\Attribute]
|
||||
class Scope extends AbstractAttribute
|
||||
{
|
||||
public function __construct(public readonly string $value)
|
||||
{
|
||||
}
|
||||
}
|
||||
9
core/bitrix/modules/rest/lib/V3/Attributes/Sortable.php
Normal file
9
core/bitrix/modules/rest/lib/V3/Attributes/Sortable.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Attributes;
|
||||
|
||||
#[\Attribute]
|
||||
class Sortable extends AbstractAttribute
|
||||
{
|
||||
|
||||
}
|
||||
30
core/bitrix/modules/rest/lib/V3/CacheManager.php
Normal file
30
core/bitrix/modules/rest/lib/V3/CacheManager.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
170
core/bitrix/modules/rest/lib/V3/Controllers/RestController.php
Normal file
170
core/bitrix/modules/rest/lib/V3/Controllers/RestController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
509
core/bitrix/modules/rest/lib/V3/Data/OrmRepository.php
Normal file
509
core/bitrix/modules/rest/lib/V3/Data/OrmRepository.php
Normal 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;
|
||||
}
|
||||
}
|
||||
34
core/bitrix/modules/rest/lib/V3/Data/Repository.php
Normal file
34
core/bitrix/modules/rest/lib/V3/Data/Repository.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Documentation\Attributes;
|
||||
use Bitrix\Rest\V3\Attributes\AbstractAttribute;
|
||||
|
||||
#[\Attribute]
|
||||
class Deprecated extends AbstractAttribute
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
77
core/bitrix/modules/rest/lib/V3/Dto/Dto.php
Normal file
77
core/bitrix/modules/rest/lib/V3/Dto/Dto.php
Normal 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;
|
||||
}
|
||||
}
|
||||
91
core/bitrix/modules/rest/lib/V3/Dto/DtoCollection.php
Normal file
91
core/bitrix/modules/rest/lib/V3/Dto/DtoCollection.php
Normal 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;
|
||||
}
|
||||
}
|
||||
18
core/bitrix/modules/rest/lib/V3/Dto/Mapping/Mapper.php
Normal file
18
core/bitrix/modules/rest/lib/V3/Dto/Mapping/Mapper.php
Normal 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;
|
||||
}
|
||||
136
core/bitrix/modules/rest/lib/V3/Dto/PropertyHelper.php
Normal file
136
core/bitrix/modules/rest/lib/V3/Dto/PropertyHelper.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Exceptions;
|
||||
|
||||
class LicenseException extends RestException
|
||||
{
|
||||
protected function getMessagePhraseCode(): string
|
||||
{
|
||||
return 'REST_LICENSE_EXCEPTION';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Exceptions;
|
||||
|
||||
class LogicException extends RestException
|
||||
{
|
||||
protected function getMessagePhraseCode(): string
|
||||
{
|
||||
return 'REST_LOGIC_EXCEPTION';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
79
core/bitrix/modules/rest/lib/V3/Exceptions/RestException.php
Normal file
79
core/bitrix/modules/rest/lib/V3/Exceptions/RestException.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Exceptions;
|
||||
|
||||
interface SkipWriteToLogException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Exceptions\Validation;
|
||||
|
||||
|
||||
class DtoValidationException extends ValidationException
|
||||
{
|
||||
protected function getMessagePhraseCode(): string
|
||||
{
|
||||
return 'REST_DTO_VALIDATION_EXCEPTION';
|
||||
}
|
||||
}
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
63
core/bitrix/modules/rest/lib/V3/Interaction/Relation.php
Normal file
63
core/bitrix/modules/rest/lib/V3/Interaction/Relation.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Interaction\Request;
|
||||
|
||||
use Bitrix\Rest\V3\Structures\FieldsStructure;
|
||||
|
||||
class AddRequest extends Request
|
||||
{
|
||||
public FieldsStructure $fields;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
122
core/bitrix/modules/rest/lib/V3/Interaction/Request/Request.php
Normal file
122
core/bitrix/modules/rest/lib/V3/Interaction/Request/Request.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Interaction\Response;
|
||||
|
||||
class AddResponse extends Response
|
||||
{
|
||||
/**
|
||||
* @param int $id
|
||||
*/
|
||||
public function __construct(public int $id)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Interaction\Response;
|
||||
|
||||
class DeleteResponse extends BooleanResponse
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Rest\V3\Interaction\Response;
|
||||
|
||||
class UpdateResponse extends BooleanResponse
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
57
core/bitrix/modules/rest/lib/V3/Schema/ControllerData.php
Normal file
57
core/bitrix/modules/rest/lib/V3/Schema/ControllerData.php
Normal 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;
|
||||
}
|
||||
}
|
||||
41
core/bitrix/modules/rest/lib/V3/Schema/MethodDescription.php
Normal file
41
core/bitrix/modules/rest/lib/V3/Schema/MethodDescription.php
Normal 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;
|
||||
}
|
||||
}
|
||||
33
core/bitrix/modules/rest/lib/V3/Schema/ModuleManager.php
Normal file
33
core/bitrix/modules/rest/lib/V3/Schema/ModuleManager.php
Normal 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
Reference in New Issue
Block a user