Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/V1Module/presenters/GroupsPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use App\Helpers\MetaFormats\Attributes\Post;
use App\Helpers\MetaFormats\Attributes\Query;
use App\Helpers\MetaFormats\Attributes\Path;
use App\Helpers\MetaFormats\Attributes\ResponseFormat;
use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat;
use App\Helpers\MetaFormats\Validators\VArray;
use App\Helpers\MetaFormats\Validators\VBool;
use App\Helpers\MetaFormats\Validators\VInt;
Expand Down Expand Up @@ -290,6 +292,7 @@ private function setGroupPoints(Request $req, Group $group): void
"If true, no admin is assigned to group (current user is assigned as admin by default.",
required: false,
)]
#[ResponseFormat(GroupFormat::class)]
public function actionAddGroup()
{
$req = $this->getRequest();
Expand Down Expand Up @@ -404,6 +407,7 @@ public function checkUpdateGroup(string $id)
#[Post("pointsLimit", new VInt(), "A minimum of (absolute) points needed to pass the course", required: false)]
#[Post("localizedTexts", new VArray(), "Localized names and descriptions")]
#[Path("id", new VUuid(), "An identifier of the updated group", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionUpdateGroup(string $id)
{
$req = $this->getRequest();
Expand Down Expand Up @@ -445,6 +449,7 @@ public function checkSetOrganizational(string $id)
*/
#[Post("value", new VBool(), "The value of the flag", required: true)]
#[Path("id", new VUuid(), "An identifier of the updated group", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionSetOrganizational(string $id)
{
$group = $this->groups->findOrThrow($id);
Expand Down Expand Up @@ -481,6 +486,7 @@ public function checkSetArchived(string $id)
*/
#[Post("value", new VBool(), "The value of the flag", required: true)]
#[Path("id", new VUuid(), "An identifier of the updated group", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionSetArchived(string $id)
{
$group = $this->groups->findOrThrow($id);
Expand Down Expand Up @@ -563,6 +569,7 @@ public function checkSetExam(string $id)
*/
#[Post("value", new VBool(), "The value of the flag", required: true)]
#[Path("id", new VUuid(), "An identifier of the updated group", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionSetExam(string $id)
{
$group = $this->groups->findOrThrow($id);
Expand Down Expand Up @@ -603,6 +610,7 @@ public function checkSetExamPeriod(string $id)
)]
#[Post("strict", new VBool(), "Whether locked users are prevented from accessing other groups.", required: false)]
#[Path("id", new VUuid(), "An identifier of the updated group", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionSetExamPeriod(string $id)
{
$group = $this->groups->findOrThrow($id);
Expand Down Expand Up @@ -716,6 +724,7 @@ public function checkRemoveExamPeriod(string $id)
* @throws NotFoundException
*/
#[Path("id", new VUuid(), "An identifier of the updated group", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionRemoveExamPeriod(string $id)
{
$group = $this->groups->findOrThrow($id);
Expand Down Expand Up @@ -862,6 +871,7 @@ public function checkDetail(string $id)
* @GET
*/
#[Path("id", new VUuid(), "Identifier of the group", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionDetail(string $id)
{
$group = $this->groups->findOrThrow($id);
Expand Down Expand Up @@ -950,6 +960,7 @@ public function checkAddMember(string $id, string $userId)
#[Post("type", new VString(1), "Identifier of membership type (admin, supervisor, ...)", required: true)]
#[Path("id", new VUuid(), "Identifier of the group", required: true)]
#[Path("userId", new VString(), "Identifier of the supervisor", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionAddMember(string $id, string $userId)
{
$user = $this->users->findOrThrow($userId);
Expand Down Expand Up @@ -996,6 +1007,7 @@ public function checkRemoveMember(string $id, string $userId)
*/
#[Path("id", new VUuid(), "Identifier of the group", required: true)]
#[Path("userId", new VString(), "Identifier of the supervisor", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionRemoveMember(string $id, string $userId)
{
$user = $this->users->findOrThrow($userId);
Expand Down Expand Up @@ -1214,6 +1226,7 @@ public function checkAddStudent(string $id, string $userId)
*/
#[Path("id", new VUuid(), "Identifier of the group", required: true)]
#[Path("userId", new VString(), "Identifier of the student", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionAddStudent(string $id, string $userId)
{
$user = $this->users->findOrThrow($userId);
Expand Down Expand Up @@ -1245,6 +1258,7 @@ public function checkRemoveStudent(string $id, string $userId)
*/
#[Path("id", new VUuid(), "Identifier of the group", required: true)]
#[Path("userId", new VString(), "Identifier of the student", required: true)]
#[ResponseFormat(GroupFormat::class)]
public function actionRemoveStudent(string $id, string $userId)
{
$user = $this->users->findOrThrow($userId);
Expand Down
10 changes: 9 additions & 1 deletion app/V1Module/presenters/UsersPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Helpers\MetaFormats\Attributes\Post;
use App\Helpers\MetaFormats\Attributes\Query;
use App\Helpers\MetaFormats\Attributes\Path;
use App\Helpers\MetaFormats\FormatDefinitions\UserFilterFormat;
use App\Helpers\MetaFormats\Validators\VArray;
use App\Helpers\MetaFormats\Validators\VBool;
use App\Helpers\MetaFormats\Validators\VEmail;
Expand All @@ -29,6 +30,7 @@
use App\Exceptions\BadRequestException;
use App\Helpers\EmailVerificationHelper;
use App\Helpers\AnonymizationHelper;
use App\Helpers\MetaFormats\Validators\VObject;
use App\Model\View\GroupViewFactory;
use App\Model\View\InstanceViewFactory;
use App\Model\View\UserViewFactory;
Expand Down Expand Up @@ -130,7 +132,13 @@ public function checkDefault()
required: false,
nullable: true,
)]
#[Query("filters", new VArray(), "Named filters that prune the result.", required: false, nullable: true)]
#[Query(
"filters",
new VObject(UserFilterFormat::class),
"Named filters that prune the result.",
required: false,
nullable: true,
)]
#[Query(
"locale",
new VString(),
Expand Down
28 changes: 24 additions & 4 deletions app/V1Module/presenters/base/BasePresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace App\V1Module\Presenters;

use App\Helpers\MetaFormats\MetaFormatHelper;
use App\Helpers\MetaFormats\Validators\VArray;
use App\Helpers\MetaFormats\Validators\VObject;
use App\Helpers\Pagination;
use App\Model\Entity\User;
use App\Security\AccessToken;
Expand Down Expand Up @@ -207,16 +209,22 @@ public function getFormatInstance(): MetaFormat

private function processParams(ReflectionMethod $reflection)
{
$actionPath = get_class($this) . $reflection->name;

// cache whether the action has a Format attribute
if (!FormatCache::formatAttributeStringCached($actionPath)) {
$extractedFormat = MetaFormatHelper::extractFormatFromAttribute($reflection);
FormatCache::cacheFormatAttributeString($actionPath, $extractedFormat);
}
// use a method specialized for formats if there is a format available
$format = MetaFormatHelper::extractFormatFromAttribute($reflection);
$format = FormatCache::getFormatAttributeString($actionPath);
if ($format !== null) {
$this->requestFormatInstance = $this->processParamsFormat($format, null);
}

// handle loose parameters

// cache the data from the loose attributes to improve performance
$actionPath = get_class($this) . $reflection->name;
if (!FormatCache::looseParametersCached($actionPath)) {
$newParamData = MetaFormatHelper::extractRequestParamData($reflection);
FormatCache::cacheLooseParameters($actionPath, $newParamData);
Expand All @@ -235,8 +243,20 @@ private function processParamsLoose(array $paramData)
foreach ($paramData as $param) {
$paramValue = $this->getValueFromParamData($param);

// this throws when it does not conform
$param->conformsToDefinition($paramValue);
// special case when the request parameter is an object (the raw request data is an array that needs to
// be mapped to the Format definition)
$mainValidator = $param->validators[0];
if ($mainValidator instanceof VObject) {
// first check whether the raw request data is an array
$param->conformsToDefinition($paramValue, [new VArray(strict: false)]);

// map the content of the array to the format
// (the created format instance is not used, because that feature is reserved for POST bodies)
$format = $mainValidator->format;
$this->processParamsFormat($format, $paramValue);
} else {
$param->conformsToDefinition($paramValue);
}
}
}

Expand Down
36 changes: 36 additions & 0 deletions app/helpers/MetaFormats/Attributes/ResponseFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Helpers\MetaFormats\Attributes;

use Attribute;

/**
* Attribute defining response format on endpoints.
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
class ResponseFormat
{
public readonly string $format;
public readonly string $description;
public readonly int $statusCode;
public readonly bool $useSuccessWrapper;

/**
* @param string $format The Format class that will be used for the response schema.
* @param string $description The description of the response.
* @param int $statusCode The status code of the response.
* @param bool $useSuccessWrapper Whether the reponse will be wrapped with
* the "BasePresenter::sendSuccessResponse" method.
*/
public function __construct(
string $format,
string $description = "Response data",
int $statusCode = 200,
bool $useSuccessWrapper = true
) {
$this->format = $format;
$this->description = $description;
$this->statusCode = $statusCode;
$this->useSuccessWrapper = $useSuccessWrapper;
}
}
30 changes: 30 additions & 0 deletions app/helpers/MetaFormats/FormatCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class FormatCache
// this array caches loose attribute data which are added over time by the presenters
private static array $actionToRequestParamDataMap = [];

// array that caches Format attribute format strings for actions
private static array $actionToFormatMap = [];

/**
* @param string $actionPath The presenter class name joined with the name of the action method.
* @return bool Returns whether the loose parameters of the action are cached.
Expand All @@ -28,6 +31,33 @@ public static function looseParametersCached(string $actionPath): bool
return array_key_exists($actionPath, self::$actionToRequestParamDataMap);
}

/**
* @param string $actionPath The presenter class name joined with the name of the action method.
* @return bool Returns whether the action Format attribute string was cached.
*/
public static function formatAttributeStringCached(string $actionPath): bool
{
return array_key_exists($actionPath, self::$actionToFormatMap);
}

/**
* @param string $actionPath The presenter class name joined with the name of the action method.
* @param string|null $format The attribute format string or null if there is none.
*/
public static function cacheFormatAttributeString(string $actionPath, string | null $format)
{
self::$actionToFormatMap[$actionPath] = $format;
}

/**
* @param string $actionPath The presenter class name joined with the name of the action method.
* @return string|null Returns action Format attribute string or null if there is no Format attribute.
*/
public static function getFormatAttributeString(string $actionPath): string | null
{
return self::$actionToFormatMap[$actionPath];
}

/**
* @param string $actionPath The presenter class name joined with the name of the action method.
* @return array Returns the cached RequestParamData array of the loose attributes.
Expand Down
74 changes: 74 additions & 0 deletions app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace App\Helpers\MetaFormats\FormatDefinitions;

use App\Helpers\MetaFormats\Attributes\Format;
use App\Helpers\MetaFormats\MetaFormat;
use App\Helpers\MetaFormats\Attributes\FPost;
use App\Helpers\MetaFormats\Validators\VArray;
use App\Helpers\MetaFormats\Validators\VBool;
use App\Helpers\MetaFormats\Validators\VObject;
use App\Helpers\MetaFormats\Validators\VString;
use App\Helpers\MetaFormats\Validators\VUuid;

/**
* Format definition used by the GroupsPresenter.
*/
#[Format(GroupFormat::class)]
class GroupFormat extends MetaFormat
{
#[FPost(new VUuid(), "An identifier of the group")]
public string $id;

#[FPost(
new VString(),
"An informative, human readable identifier of the group",
required: false,
nullable: true,
)]
public ?string $externalId;

#[FPost(
new VBool(),
"Whether the group is organizational (no assignments nor students).",
required: false,
)]
public ?bool $organizational;

#[FPost(new VBool(), "Whether the group is an exam group.", required: false)]
public ?bool $exam;

#[FPost(new VBool(), "Whether the group is archived", required: false)]
public ?bool $archived;

#[FPost(new VBool(), "Should the group be visible to all student?")]
public ?bool $public;

#[FPost(new VBool(), "Whether the group was explicitly marked as archived")]
public ?bool $directlyArchived;

#[FPost(new VArray(), "Localized names and descriptions", required: false)]
public ?array $localizedTexts;

#[FPost(new VArray(new VUuid()), "IDs of users which are explicitly listed as direct admins of this group")]
public ?array $primaryAdminsIds;

#[FPost(
new VUuid(),
"Identifier of the parent group (absent for a top-level group)",
required: false,
)]
public ?string $parentGroupId;

#[FPost(new VArray(new VUuid()), "Identifications of groups in descending order.")]
public ?array $parentGroupsIds;

#[FPost(new VArray(new VUuid()), "Identifications of child groups.")]
public ?array $childGroups;

#[FPost(new VObject(GroupPrivateDataFormat::class), required: false)]
public ?GroupPrivateDataFormat $privateData;

#[FPost(new VArray())]
public ?array $permissionHints;
}
Loading