Merge branch '1.7.x' into update-tokens

This commit is contained in:
Darshan
2025-05-13 16:16:46 +05:30
committed by GitHub
130 changed files with 6504 additions and 151513 deletions
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Appwrite\Deletes;
use Utopia\Database\Database;
use Utopia\Database\Query;
class Identities
{
public static function delete(Database $database, Query $query): void
{
$database->deleteDocuments(
'identities',
[
$query,
Query::orderAsc()
],
Database::DELETE_BATCH_SIZE
);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Appwrite\Deletes;
use Appwrite\Extend\Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
class Targets
{
public static function delete(Database $database, Query $query): void
{
$database->deleteDocuments(
'targets',
[
$query,
Query::orderAsc()
],
Database::DELETE_BATCH_SIZE,
fn (Document $target) => self::deleteSubscribers($database, $target)
);
}
public static function deleteSubscribers(Database $database, Document $target): void
{
$database->deleteDocuments(
'subscribers',
[
Query::equal('targetInternalId', [$target->getInternalId()]),
Query::orderAsc(),
],
Database::DELETE_BATCH_SIZE,
function (Document $subscriber) use ($database, $target) {
$topicId = $subscriber->getAttribute('topicId');
$topicInternalId = $subscriber->getAttribute('topicInternalId');
$topic = $database->getDocument('topics', $topicId);
if (!$topic->isEmpty() && $topic->getInternalId() === $topicInternalId) {
$totalAttribute = match ($target->getAttribute('providerType')) {
MESSAGE_TYPE_EMAIL => 'emailTotal',
MESSAGE_TYPE_SMS => 'smsTotal',
MESSAGE_TYPE_PUSH => 'pushTotal',
default => throw new Exception('Invalid target provider type'),
};
$database->decreaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
min: 0
);
}
}
);
}
}
+10 -4
View File
@@ -117,13 +117,19 @@ class Mapper
$list = false;
foreach ($route->getParams() as $name => $parameter) {
$methodParameters = $method->getParameters();
$sdkParameters = $method->getParameters();
if (!empty($methodParameters)) {
if (!array_key_exists($name, $methodParameters)) {
if (!empty($sdkParameters)) {
$sdkMethodParameters = [];
foreach ($sdkParameters as $sdkParameter) {
$sdkMethodParameters[$sdkParameter->getName()] = $sdkParameter;
}
if (!\array_key_exists($name, $sdkMethodParameters)) {
continue;
}
$optional = $methodParameters[$name]['optional'];
$optional = $sdkMethodParameters[$name]->getOptional();
} else {
$optional = $parameter['optional'];
}
+5 -4
View File
@@ -9,6 +9,7 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\PDO;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\System\System;
@@ -38,9 +39,9 @@ abstract class Migration
protected Database $consoleDB;
/**
* @var \PDO
* @var PDO
*/
protected \PDO $pdo;
protected PDO $pdo;
/**
* @var array
@@ -147,10 +148,10 @@ abstract class Migration
/**
* Set PDO for Migration.
*
* @param \PDO $pdo
* @param PDO $pdo
* @return \Appwrite\Migration\Migration
*/
public function setPDO(\PDO $pdo): self
public function setPDO(PDO $pdo): self
{
$this->pdo = $pdo;
@@ -34,9 +34,9 @@ class Specification extends Validator
$allowedSpecifications = [];
foreach ($this->specifications as $size => $values) {
if ($values['cpus'] <= $this->maxCpus && $values['memory'] <= $this->maxMemory) {
if (!empty($this->plan) && array_key_exists('specifications', $this->plan)) {
if (!\in_array($size, $this->plan['specifications'])) {
if ((empty($this->maxCpus) || $values['cpus'] <= $this->maxCpus) && (empty($this->maxMemory) || $values['memory'] <= $this->maxMemory)) {
if (!empty($this->plan) && array_key_exists('runtimeSpecifications', $this->plan)) {
if (!\in_array($size, $this->plan['runtimeSpecifications'])) {
continue;
}
}
@@ -6,6 +6,7 @@ use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
@@ -68,7 +69,7 @@ class Create extends Action
model: Response::MODEL_DEPLOYMENT,
)
],
requestType: 'multipart/form-data',
requestType: ContentType::MULTIPART,
type: MethodType::UPLOAD,
packaging: true,
))
@@ -75,7 +75,6 @@ class Create extends Base
)
],
contentType: ContentType::MULTIPART,
requestType: 'application/json',
))
->param('functionId', '', new UID(), 'Function ID.')
->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true)
@@ -14,7 +14,6 @@ use Appwrite\Task\Validator\Cron;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -89,8 +88,8 @@ class Create extends Base
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function and builds.', true, ['plan'])
->inject('response')
->inject('dbForProject')
@@ -131,7 +130,7 @@ class Create extends Base
// Temporary abuse check
$abuseCheck = function () use ($project, $timelimit, $response) {
$abuseKey = "projectId:{projectId},url:{url}";
$abuseLimit = App::getEnv('_APP_FUNCTIONS_CREATION_ABUSE_LIMIT', 50);
$abuseLimit = System::getEnv('_APP_FUNCTIONS_CREATION_ABUSE_LIMIT', 50);
$abuseTime = 86400; // 1 day
$timeLimit = $timelimit($abuseKey, $abuseLimit, $abuseTime);
@@ -14,7 +14,6 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Task\Validator\Cron;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -93,8 +92,8 @@ class Update extends Base
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function and builds.', true, ['plan'])
->inject('request')
->inject('response')
@@ -59,12 +59,16 @@ class XList extends Base
foreach ($allSpecs as $spec) {
$spec['enabled'] = true;
if (array_key_exists('specifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['specifications']);
if (array_key_exists('runtimeSpecifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
}
$maxCpus = System::getEnv('_APP_FUNCTIONS_CPUS', 0);
$maxMemory = System::getEnv('_APP_FUNCTIONS_MEMORY', 0);
// Only add specs that are within the limits set by environment variables
if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) {
// Treat 0 as no limit
if ((empty($maxCpus) || $spec['cpus'] <= $maxCpus) && (empty($maxMemory) || $spec['memory'] <= $maxMemory)) {
$specs[] = $spec;
}
}
@@ -6,6 +6,7 @@ use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
@@ -65,7 +66,7 @@ class Create extends Action
model: Response::MODEL_DEPLOYMENT,
)
],
requestType: 'multipart/form-data',
requestType: ContentType::MULTIPART,
type: MethodType::UPLOAD,
packaging: true,
))
@@ -11,7 +11,6 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -82,8 +81,8 @@ class Create extends Base
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Framework specification for the site and builds.', true, ['plan'])
->inject('response')
->inject('dbForProject')
@@ -12,7 +12,6 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -86,8 +85,8 @@ class Update extends Base
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Framework specification for the site and builds.', true, ['plan'])
->inject('request')
->inject('response')
@@ -59,12 +59,16 @@ class XList extends Base
foreach ($allSpecs as $spec) {
$spec['enabled'] = true;
if (array_key_exists('specifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['specifications']);
if (array_key_exists('runtimeSpecifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
}
$maxCpus = System::getEnv('_APP_FUNCTIONS_CPUS', 0);
$maxMemory = System::getEnv('_APP_FUNCTIONS_MEMORY', 0);
// Only add specs that are within the limits set by environment variables
if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) {
// Treat 0 as no limit
if ((empty($maxCpus) || $spec['cpus'] <= $maxCpus) && (empty($maxMemory) || $spec['memory'] <= $maxMemory)) {
$specs[] = $spec;
}
}
+18 -3
View File
@@ -35,12 +35,27 @@ class Maintenance extends Action
Console::title('Maintenance V1');
Console::success(APP_NAME . ' maintenance process v1 has started');
// # of days in seconds (1 day = 86400s)
$interval = (int) System::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
$delay = (int) System::getEnv('_APP_MAINTENANCE_DELAY', '0');
$interval = (int) System::getEnv('_APP_MAINTENANCE_INTERVAL', '86400'); // 1 day
$usageStatsRetentionHourly = (int) System::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_HOURLY', '8640000'); //100 days
$cacheRetention = (int) System::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days
$schedulesDeletionRetention = (int) System::getEnv('_APP_MAINTENANCE_RETENTION_SCHEDULES', '86400'); // 1 Day
$jobInitTime = System::getEnv('_APP_MAINTENANCE_START_TIME', '00:00'); // (hour:minutes)
$now = new \DateTime();
$now->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$next = new \DateTime($now->format("Y-m-d $jobInitTime"));
$next->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$delay = $next->getTimestamp() - $now->getTimestamp();
/**
* If time passed for the target day.
*/
if ($delay <= 0) {
$next->add(\DateInterval::createFromDateString('1 days'));
$delay = $next->getTimestamp() - $now->getTimestamp();
}
Console::info('Setting loop start time to ' . $next->format("Y-m-d H:i:s.v") . '. Delaying for ' . $delay . ' seconds.');
Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) {
$time = DateTime::now();
+1 -1
View File
@@ -33,7 +33,7 @@ class Migrate extends Action
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('register')
->callback([$this, 'action']);
->callback($this->action(...));
}
private function clearProjectsCache(Document $project)
+91 -94
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Tasks;
use Swoole\Runtime;
use Swoole\Timer;
use Utopia\CLI\Console;
use Utopia\Database\Database;
@@ -13,8 +14,9 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Pools\Group;
use Utopia\System\System;
use function Swoole\Coroutine\run;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Gauge;
use Utopia\Telemetry\Histogram;
abstract class ScheduleBase extends Action
{
@@ -23,6 +25,11 @@ abstract class ScheduleBase extends Action
protected array $schedules = [];
private ?Histogram $collectSchedulesTelemetryDuration = null;
private ?Gauge $collectSchedulesTelemetryCount = null;
private ?Gauge $scheduleTelemetryCount = null;
private ?Histogram $enqueueDelayTelemetry = null;
abstract public static function getName(): string;
abstract public static function getSupportedResource(): string;
abstract public static function getCollectionId(): string;
@@ -37,7 +44,8 @@ abstract class ScheduleBase extends Action
->inject('pools')
->inject('dbForPlatform')
->inject('getProjectDB')
->callback([$this, 'action']);
->inject('telemetry')
->callback($this->action(...));
}
protected function updateProjectAccess(Document $project, Database $dbForPlatform): void
@@ -56,11 +64,44 @@ abstract class ScheduleBase extends Action
* 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute
* 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker.
*/
public function action(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
public function action(Group $pools, Database $dbForPlatform, callable $getProjectDB, Telemetry $telemetry): void
{
Runtime::enableCoroutine();
Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1');
Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started');
$this->scheduleTelemetryCount = $telemetry->createGauge('task.schedule.count');
$this->collectSchedulesTelemetryDuration = $telemetry->createHistogram('task.schedule.collect_schedules.duration', 's');
$this->collectSchedulesTelemetryCount = $telemetry->createGauge('task.schedule.collect_schedules.count');
$this->enqueueDelayTelemetry = $telemetry->createHistogram('task.schedule.enqueue_delay', 's');
// start with "0" to load all active documents.
$lastSyncUpdate = "0";
$this->collectSchedules($pools, $dbForPlatform, $getProjectDB, $lastSyncUpdate);
Console::success("Starting timers at " . DateTime::now());
/**
* The timer synchronize $schedules copy with database collection.
*/
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($pools, $dbForPlatform, $getProjectDB, &$lastSyncUpdate) {
$time = DateTime::now();
Console::log("Sync tick: Running at $time");
$this->collectSchedules($pools, $dbForPlatform, $getProjectDB, $lastSyncUpdate);
});
while (true) {
$this->enqueueResources($pools, $dbForPlatform, $getProjectDB);
$this->scheduleTelemetryCount->record(count($this->schedules), ['resourceType' => static::getSupportedResource()]);
sleep(static::ENQUEUE_TIMER);
}
}
private function collectSchedules(Group $pools, Database $dbForPlatform, callable $getProjectDB, ?string &$lastSyncUpdate): void
{
// If we haven't synced yet, load all active schedules
$initialLoad = $lastSyncUpdate === "0";
/**
* Extract only necessary attributes to lower memory used.
*
@@ -68,7 +109,7 @@ abstract class ScheduleBase extends Action
* @throws Exception
* @var Document $schedule
*/
$getSchedule = function (Document $schedule) use ($dbForPlatform, $getProjectDB): array {
$getSchedule = function (Document $schedule) use ($pools, $dbForPlatform, $getProjectDB): array {
$project = $dbForPlatform->getDocument('projects', $schedule->getAttribute('projectId'));
$resource = $getProjectDB($project)->getDocument(
@@ -76,6 +117,8 @@ abstract class ScheduleBase extends Action
$schedule->getAttribute('resourceId')
);
$pools->reclaim();
return [
'$internalId' => $schedule->getInternalId(),
'$id' => $schedule->getId(),
@@ -88,12 +131,12 @@ abstract class ScheduleBase extends Action
];
};
$lastSyncUpdate = DateTime::now();
$loadStart = microtime(true);
$time = DateTime::now();
$limit = 10_000;
$sum = $limit;
$total = 0;
$loadStart = \microtime(true);
$latestDocument = null;
while ($sum === $limit) {
@@ -110,105 +153,59 @@ abstract class ScheduleBase extends Action
$regions[] = 'default';
}
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
$paginationQueries = [
...$paginationQueries,
Query::equal('region', $regions),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::equal('active', [true]),
]));
];
$sum = \count($results);
if ($initialLoad) {
$paginationQueries[] = Query::equal('active', [true]);
} else {
$paginationQueries[] = Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate);
}
$results = $dbForPlatform->find('schedules', $paginationQueries);
$sum = count($results);
$total = $total + $sum;
foreach ($results as $document) {
try {
$this->schedules[$document->getInternalId()] = $getSchedule($document);
} catch (\Throwable $th) {
$collectionId = static::getCollectionId();
Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}");
Console::error($th->getMessage());
$localDocument = $this->schedules[$document->getInternalId()] ?? null;
if ($localDocument !== null) {
if (!$document['active']) {
Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}");
unset($this->schedules[$document->getInternalId()]);
} elseif (strtotime($localDocument['resourceUpdatedAt']) !== strtotime($document['resourceUpdatedAt'])) {
Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}");
$this->schedules[$document->getInternalId()] = $getSchedule($document);
}
} else {
try {
$this->schedules[$document->getInternalId()] = $getSchedule($document);
} catch (\Throwable $th) {
$collectionId = static::getCollectionId();
Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}");
Console::error($th->getMessage());
}
}
}
$latestDocument = \end($results);
}
$pools->reclaim();
$lastSyncUpdate = $time;
$duration = microtime(true) - $loadStart;
$this->collectSchedulesTelemetryDuration->record($duration, ['initial' => $initialLoad, 'resourceType' => static::getSupportedResource()]);
$this->collectSchedulesTelemetryCount->record($total, ['initial' => $initialLoad, 'resourceType' => static::getSupportedResource()]);
Console::success("{$total} resources were loaded in " . $duration . " seconds");
}
Console::success("{$total} resources were loaded in " . (\microtime(true) - $loadStart) . " seconds");
Console::success("Starting timers at " . DateTime::now());
run(function () use ($dbForPlatform, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) {
/**
* The timer synchronize $schedules copy with database collection.
*/
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForPlatform, &$lastSyncUpdate, $getSchedule, $pools) {
$time = DateTime::now();
$timerStart = \microtime(true);
$limit = 1000;
$sum = $limit;
$total = 0;
$latestDocument = null;
Console::log("Sync tick: Running at $time");
while ($sum === $limit) {
$paginationQueries = [Query::limit($limit)];
if ($latestDocument) {
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
// Temporarly accepting both 'fra' and 'default'
// When all migrated, only use _APP_REGION with 'default' as default value
$regions = [System::getEnv('_APP_REGION', 'default')];
if (!in_array('default', $regions)) {
$regions[] = 'default';
}
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', $regions),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
]));
$sum = count($results);
$total = $total + $sum;
foreach ($results as $document) {
$localDocument = $this->schedules[$document->getInternalId()] ?? null;
// Check if resource has been updated since last sync
$org = $localDocument !== null ? \strtotime($localDocument['resourceUpdatedAt']) : null;
$new = \strtotime($document['resourceUpdatedAt']);
if (!$document['active']) {
Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}");
unset($this->schedules[$document->getInternalId()]);
} elseif ($new !== $org) {
Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}");
$this->schedules[$document->getInternalId()] = $getSchedule($document);
}
}
$latestDocument = \end($results);
}
$lastSyncUpdate = $time;
$timerEnd = \microtime(true);
$pools->reclaim();
Console::log("Sync tick: {$total} schedules were updated in " . ($timerEnd - $timerStart) . " seconds");
});
Timer::tick(
static::ENQUEUE_TIMER * 1000,
fn () => $this->enqueueResources($pools, $dbForPlatform, $getProjectDB)
);
$this->enqueueResources($pools, $dbForPlatform, $getProjectDB);
});
protected function recordEnqueueDelay(string $expectedExecutionSchedule): void
{
$now = strtotime('now');
$scheduledAt = strtotime($expectedExecutionSchedule);
$this->enqueueDelayTelemetry->record($now - $scheduledAt, ['resourceType' => static::getSupportedResource()]);
}
}
@@ -74,6 +74,8 @@ class ScheduleExecutions extends ScheduleBase
->setProject($schedule['project'])
->setUserId($data['userId'] ?? '')
->trigger();
$this->recordEnqueueDelay($schedule['schedule']);
});
$dbForPlatform->deleteDocument(
@@ -95,6 +95,8 @@ class ScheduleFunctions extends ScheduleBase
->setPath('/')
->setProject($schedule['project'])
->trigger();
$this->recordEnqueueDelay($schedule['schedule']);
}
$queue->reclaim();
@@ -59,7 +59,7 @@ class ScheduleMessages extends ScheduleBase
);
$queue->reclaim();
$this->recordEnqueueDelay($schedule['schedule']);
unset($this->schedules[$schedule['$internalId']]);
});
}
+16 -10
View File
@@ -3,9 +3,10 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\SDK\AuthType;
use Appwrite\Specification\Format\OpenAPI3;
use Appwrite\Specification\Format\Swagger2;
use Appwrite\Specification\Specification;
use Appwrite\SDK\Method;
use Appwrite\SDK\Specification\Format\OpenAPI3;
use Appwrite\SDK\Specification\Format\Swagger2;
use Appwrite\SDK\Specification\Specification;
use Appwrite\Utopia\Request as AppwriteRequest;
use Appwrite\Utopia\Response as AppwriteResponse;
use Exception;
@@ -19,7 +20,6 @@ use Utopia\Config\Config;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Database;
use Utopia\Platform\Action;
use Utopia\Registry\Registry;
use Utopia\Request as UtopiaRequest;
use Utopia\Response as UtopiaResponse;
use Utopia\System\System;
@@ -49,11 +49,10 @@ class Specs extends Action
->desc('Generate Appwrite API specifications')
->param('version', 'latest', new Text(16), 'Spec version', true)
->param('mode', 'normal', new WhiteList(['normal', 'mocks']), 'Spec Mode', true)
->inject('register')
->callback([$this, 'action']);
->callback($this->action(...));
}
public function action(string $version, string $mode, Registry $register): void
public function action(string $version, string $mode): void
{
$appRoutes = App::getRoutes();
$response = $this->getResponse();
@@ -194,7 +193,7 @@ class Specs extends Action
}
foreach ($sdks as $sdk) {
/** @var \Appwrite\SDK\Method $sdks */
/** @var Method $sdk */
$hide = $sdk->isHidden();
if ($hide === true || (\is_array($hide) && \in_array($platform, $hide))) {
@@ -262,7 +261,6 @@ class Specs extends Action
$services[] = [
'name' => $service['key'] ?? '',
'description' => $service['subtitle'] ?? '',
'x-globalAttributes' => $service['globalAttributes'] ?? [],
];
}
@@ -274,7 +272,15 @@ class Specs extends Action
}
}
$arguments = [new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0];
$arguments = [
new App('UTC'),
$services,
$routes,
$models,
$keys[$platform],
$authCounts[$platform] ?? 0
];
foreach (['swagger2', 'open-api3'] as $format) {
$formatInstance = match ($format) {
'swagger2' => new Swagger2(...$arguments),
+7 -79
View File
@@ -4,6 +4,8 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Auth\Auth;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Deletes\Identities;
use Appwrite\Deletes\Targets;
use Appwrite\Extend\Exception;
use Executor\Executor;
use Throwable;
@@ -166,7 +168,7 @@ class Deletes extends Action
$this->deleteTopic($project, $getProjectDB, $document);
break;
case DELETE_TYPE_TARGET:
$this->deleteTargetSubscribers($project, $getProjectDB, $document);
Targets::deleteSubscribers($getProjectDB($project), $document);
break;
case DELETE_TYPE_EXPIRED_TARGETS:
$this->deleteExpiredTargets($project, $getProjectDB);
@@ -282,47 +284,6 @@ class Deletes extends Action
);
}
/**
* @param Document $project
* @param callable $getProjectDB
* @param Document $target
* @throws Exception
*/
private function deleteTargetSubscribers(Document $project, callable $getProjectDB, Document $target): void
{
/** @var Database */
$dbForProject = $getProjectDB($project);
// Delete subscribers and decrement topic counts
$this->deleteByGroup(
'subscribers',
[
Query::equal('targetInternalId', [$target->getInternalId()]),
Query::orderAsc(),
],
$dbForProject,
function (Document $subscriber) use ($dbForProject, $target) {
$topicId = $subscriber->getAttribute('topicId');
$topicInternalId = $subscriber->getAttribute('topicInternalId');
$topic = $dbForProject->getDocument('topics', $topicId);
if (!$topic->isEmpty() && $topic->getInternalId() === $topicInternalId) {
$totalAttribute = match ($target->getAttribute('providerType')) {
MESSAGE_TYPE_EMAIL => 'emailTotal',
MESSAGE_TYPE_SMS => 'smsTotal',
MESSAGE_TYPE_PUSH => 'pushTotal',
default => throw new Exception('Invalid target CertificatesAdapter type'),
};
$dbForProject->decreaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
min: 0
);
}
}
);
}
/**
* @param Document $project
* @param callable $getProjectDB
@@ -332,32 +293,12 @@ class Deletes extends Action
*/
private function deleteExpiredTargets(Document $project, callable $getProjectDB): void
{
$this->deleteByGroup(
'targets',
[
Query::equal('expired', [true]),
Query::orderAsc(),
],
$getProjectDB($project),
function (Document $target) use ($getProjectDB, $project) {
$this->deleteTargetSubscribers($project, $getProjectDB, $target);
}
);
Targets::delete($getProjectDB($project), Query::equal('expired', [true]));
}
private function deleteSessionTargets(Document $project, callable $getProjectDB, Document $session): void
{
$this->deleteByGroup(
'targets',
[
Query::equal('sessionInternalId', [$session->getInternalId()]),
Query::orderAsc(),
],
$getProjectDB($project),
function (Document $target) use ($getProjectDB, $project) {
$this->deleteTargetSubscribers($project, $getProjectDB, $target);
}
);
Targets::delete($getProjectDB($project), Query::equal('sessionInternalId', [$session->getInternalId()]));
}
/**
@@ -742,23 +683,10 @@ class Deletes extends Action
], $dbForProject);
// Delete identities
$this->deleteByGroup('identities', [
Query::equal('userInternalId', [$userInternalId]),
Query::orderAsc()
], $dbForProject);
Identities::delete($dbForProject, Query::equal('userInternalId', [$userInternalId]));
// Delete targets
$this->deleteByGroup(
'targets',
[
Query::equal('userInternalId', [$userInternalId]),
Query::orderAsc()
],
$dbForProject,
function (Document $target) use ($getProjectDB, $project) {
$this->deleteTargetSubscribers($project, $getProjectDB, $target);
}
);
Targets::delete($dbForProject, Query::equal('userInternalId', [$userInternalId]));
}
/**
+12 -3
View File
@@ -38,6 +38,13 @@ class Migrations extends Action
protected Document $project;
/**
* Cached for performance.
*
* @var array<string, int>
*/
protected array $sourceReport = [];
/**
* @var callable
*/
@@ -109,7 +116,7 @@ class Migrations extends Action
$credentials = $migration->getAttribute('credentials');
$migrationOptions = $migration->getAttribute('options');
return match ($source) {
$migrationSource = match ($source) {
Firebase::getName() => new Firebase(
json_decode($credentials['serviceAccount'], true),
),
@@ -144,6 +151,10 @@ class Migrations extends Action
),
default => throw new \Exception('Invalid source type'),
};
$this->sourceReport = $migrationSource->report();
return $migrationSource;
}
/**
@@ -262,8 +273,6 @@ class Migrations extends Action
$source = $this->processSource($migration);
$destination = $this->processDestination($migration, $tempAPIKey);
$source->report();
$transfer = new Transfer(
$source,
$destination
+10 -10
View File
@@ -21,16 +21,14 @@ class Method
* @param string $description
* @param array<AuthType> $auth
* @param array<SDKResponse> $responses
* @param ContentType $responseType
* @param MethodType|null $methodType
* @param ContentType $contentType
* @param MethodType|null $type
* @param bool $deprecated
* @param array|bool $hide
* @param bool $packaging
* @param string $requestType
* @param array $parameters
* @param ContentType $requestType
* @param array<Parameter> $parameters
* @param array $additionalParameters
*
* @throws \Exception
*/
public function __construct(
protected string $namespace,
@@ -44,7 +42,7 @@ class Method
protected bool $deprecated = false,
protected array|bool $hide = false,
protected bool $packaging = false,
protected string $requestType = 'application/json',
protected ContentType $requestType = ContentType::JSON,
protected array $parameters = [],
protected array $additionalParameters = []
) {
@@ -53,7 +51,6 @@ class Method
$this->validateDesc($description);
foreach ($responses as $response) {
/** @var SDKResponse $response */
$this->validateResponseModel($response->getModel());
$this->validateNoContent($response);
}
@@ -193,11 +190,14 @@ class Method
return $this->packaging;
}
public function getRequestType(): string
public function getRequestType(): ContentType
{
return $this->requestType;
}
/**
* @return array<Parameter>
*/
public function getParameters(): array
{
return $this->parameters;
@@ -276,7 +276,7 @@ class Method
return $this;
}
public function setRequestType(string $requestType): self
public function setRequestType(ContentType $requestType): self
{
$this->requestType = $requestType;
return $this;
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace Appwrite\SDK;
use Utopia\Validator;
class Parameter
{
/**
* @param string $name
* @param string $description
* @param mixed|null $default
* @param Validator|callable|null $validator
* @param bool $optional
*/
public function __construct(
protected string $name,
protected string $description = '',
protected mixed $default = null,
protected mixed $validator = null,
protected bool $optional = false,
) {
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
public function getDefault(): mixed
{
return $this->default;
}
public function setDefault(mixed $default): static
{
$this->default = $default;
return $this;
}
public function getValidator(): mixed
{
return $this->validator;
}
public function setValidator(mixed $validator): static
{
$this->validator = $validator;
return $this;
}
public function getOptional(): bool
{
return $this->optional;
}
public function setOptional(bool $optional): static
{
$this->optional = $optional;
return $this;
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Specification;
namespace Appwrite\SDK\Specification;
use Appwrite\Utopia\Response\Model;
use Utopia\App;
@@ -12,12 +12,12 @@ abstract class Format
protected App $app;
/**
* @var Route[]
* @var array<Route>
*/
protected array $routes;
/**
* @var Model[]
* @var array<Model>
*/
protected array $models;
@@ -487,4 +487,24 @@ abstract class Format
}
return $values;
}
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (!in_array($model->getType(), $usedModels)) {
continue;
}
$types = (array)$rule['type'];
foreach ($types as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $ruleType;
foreach ($this->models as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedModels($m, $usedModels);
}
}
}
}
}
}
}
@@ -1,10 +1,12 @@
<?php
namespace Appwrite\Specification\Format;
namespace Appwrite\SDK\Specification\Format;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\Specification\Format;
use Appwrite\SDK\Response;
use Appwrite\SDK\Specification\Format;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Helpers\Permission;
@@ -22,41 +24,6 @@ class OpenAPI3 extends Format
return 'Open API 3';
}
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (!in_array($model->getType(), $usedModels)) {
continue;
}
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $ruleType;
foreach ($this->models as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
} else {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
}
}
public function parse(): array
{
/**
@@ -133,22 +100,20 @@ class OpenAPI3 extends Format
}
$additionalMethods = null;
if (is_array($sdk)) {
$mainSdk = array_shift($sdk);
if (\is_array($sdk)) {
$additionalMethods = $sdk;
$sdk = $mainSdk;
$sdk = $sdk[0];
}
/**
* @var \Appwrite\SDK\Method $sdk
* @var Method $sdk
*/
$consumes = [$sdk->getRequestType()];
$consumes = [$sdk->getRequestType()->value];
$method = $sdk->getMethodName() ?? \uniqid();
if (!empty($method) && is_array($method)) {
$method = array_keys($method)[0];
if (!empty($method) && \is_array($method)) {
$method = \array_keys($method)[0];
}
$desc = $sdk->getDescriptionFilePath() ?: $sdk->getDescription();
@@ -161,10 +126,8 @@ class OpenAPI3 extends Format
case AuthType::SESSION:
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
break;
case AuthType::KEY:
$sdkPlatforms[] = APP_PLATFORM_SERVER;
break;
case AuthType::JWT:
case AuthType::KEY:
$sdkPlatforms[] = APP_PLATFORM_SERVER;
break;
case AuthType::ADMIN:
@@ -209,39 +172,47 @@ class OpenAPI3 extends Format
if (!empty($additionalMethods)) {
$temp['x-appwrite']['additional-methods'] = [];
$temp['x-appwrite']['methods'] = [];
foreach ($additionalMethods as $method) {
/** @var \Appwrite\SDK\Method $method */
/** @var Method $method */
$desc = $method->getDescriptionFilePath();
$additionalMethod = [
'name' => $method->getMethodName(),
'parameters' => [],
'required' => [],
'responses' => []
'responses' => [],
'description' => ($desc) ? \file_get_contents($desc) : '',
];
foreach ($method->getParameters() as $name => $param) {
$additionalMethod['parameters'][] = $name;
foreach ($method->getParameters() as $parameter) {
$additionalMethod['parameters'][] = $parameter->getName();
if (!$param['optional']) {
$additionalMethod['required'][] = $name;
if (!$parameter->getOptional()) {
$additionalMethod['required'][] = $parameter->getName();
}
}
foreach ($method->getResponses() as $response) {
/** @var \Appwrite\SDK\Response $response */
$additionalMethod['responses'][] = [
'code' => $response->getCode(),
'model' => '#/components/schemas/' . $response->getModel()
];
if (\is_array($response->getModel())) {
$additionalMethod['responses'][] = [
'code' => $response->getCode(),
'model' => \array_map(fn ($m) => '#/components/schemas/' . $m, $response->getModel())
];
} else {
$additionalMethod['responses'][] = [
'code' => $response->getCode(),
'model' => '#/components/schemas/' . $response->getModel()
];
}
}
$temp['x-appwrite']['additional-methods'][] = $additionalMethod;
$temp['x-appwrite']['methods'][] = $additionalMethod;
}
}
// Handle response models
foreach ($sdk->getResponses() as $response) {
/** @var \Appwrite\SDK\Response $response */
/** @var Response $response */
$model = $response->getModel();
foreach ($this->models as $value) {
@@ -309,11 +280,11 @@ class OpenAPI3 extends Format
}
}
if ((!empty($scope))) { // && 'public' != $scope
if ((!empty($scope))) {
$securities = ['Project' => []];
foreach ($sdk->getAuth() as $security) {
/** @var \Appwrite\SDK\AuthType $security */
/** @var AuthType $security */
if (array_key_exists($security->value, $this->keys)) {
$securities[$security->value] = [];
}
@@ -1,18 +1,22 @@
<?php
namespace Appwrite\Specification\Format;
namespace Appwrite\SDK\Specification\Format;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\Specification\Format;
use Appwrite\SDK\Response;
use Appwrite\SDK\Specification\Format;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Route;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Swagger2 extends Format
{
@@ -21,41 +25,6 @@ class Swagger2 extends Format
return 'Swagger 2';
}
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (!in_array($model->getType(), $usedModels)) {
continue;
}
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $ruleType;
foreach ($this->models as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
} else {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
}
}
public function parse(): array
{
/*
@@ -118,11 +87,12 @@ class Swagger2 extends Format
$usedModels = [];
foreach ($this->routes as $route) {
/** @var \Utopia\Route $route */
/** @var Route $route */
$url = \str_replace('/v1', '', $route->getPath());
$scope = $route->getLabel('scope', '');
/** @var \Appwrite\SDK\Method $sdk */
/** @var Method $sdk */
$sdk = $route->getLabel('sdk', false);
if (empty($sdk)) {
@@ -130,16 +100,14 @@ class Swagger2 extends Format
}
$additionalMethods = null;
if (is_array($sdk)) {
$mainSdk = array_shift($sdk);
if (\is_array($sdk)) {
$additionalMethods = $sdk;
$sdk = $mainSdk;
$sdk = $sdk[0];
}
$consumes = [];
if (strtoupper($route->getMethod()) !== 'GET' && strtoupper($route->getMethod()) !== 'HEAD') {
$consumes = [$sdk->getRequestType()];
$consumes = [$sdk->getRequestType()->value];
}
$method = $sdk->getMethodName() ?? \uniqid();
@@ -158,10 +126,8 @@ class Swagger2 extends Format
case AuthType::SESSION:
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
break;
case AuthType::KEY:
$sdkPlatforms[] = APP_PLATFORM_SERVER;
break;
case AuthType::JWT:
case AuthType::KEY:
$sdkPlatforms[] = APP_PLATFORM_SERVER;
break;
case AuthType::ADMIN:
@@ -211,41 +177,49 @@ class Swagger2 extends Format
}
if (!empty($additionalMethods)) {
$temp['x-appwrite']['additional-methods'] = [];
$temp['x-appwrite']['methods'] = [];
foreach ($additionalMethods as $method) {
/** @var \Appwrite\SDK\Method $method */
/** @var Method $method */
$desc = $method->getDescriptionFilePath();
$additionalMethod = [
'name' => $method->getMethodName(),
'parameters' => [],
'required' => [],
'responses' => [],
'description' => $method->getDescription(),
'description' => ($desc) ? \file_get_contents($desc) : '',
];
foreach ($method->getParameters() as $name => $param) {
$additionalMethod['parameters'][] = $name;
foreach ($method->getParameters() as $parameter) {
$additionalMethod['parameters'][] = $parameter->getName();
if (!$param['optional']) {
$additionalMethod['required'][] = $name;
if (!$parameter->getOptional()) {
$additionalMethod['required'][] = $parameter->getName();
}
}
foreach ($method->getResponses() as $response) {
/** @var \Appwrite\SDK\Response $response */
$additionalMethod['responses'][] = [
'code' => $response->getCode(),
'model' => '#/definitions/' . $response->getModel()
];
/** @var Response $response */
if (\is_array($response->getModel())) {
$additionalMethod['responses'][] = [
'code' => $response->getCode(),
'model' => \array_map(fn ($m) => '#/definitions/' . $m, $response->getModel())
];
} else {
$additionalMethod['responses'][] = [
'code' => $response->getCode(),
'model' => '#/definitions/' . $response->getModel()
];
}
}
$temp['x-appwrite']['additional-methods'][] = $additionalMethod;
$temp['x-appwrite']['methods'][] = $additionalMethod;
}
}
// Handle Responses
foreach ($sdk->getResponses() as $response) {
/** @var \Appwrite\SDK\Response $response */
/** @var Response $response */
$model = $response->getModel();
foreach ($this->models as $value) {
@@ -340,7 +314,9 @@ class Swagger2 extends Format
foreach ($parameters as $name => $param) { // Set params
/** @var Validator $validator */
$validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator'];
$validator = (\is_callable($param['validator']))
? ($param['validator'])(...$this->app->getResources($param['injections']))
: $param['validator'];
$node = [
'name' => $name,
@@ -485,7 +461,7 @@ class Swagger2 extends Format
$node['type'] = $validator->getType();
break;
case 'Utopia\Validator\WhiteList':
/** @var \Utopia\Validator\WhiteList $validator */
/** @var WhiteList $validator */
$node['type'] = $validator->getType();
$node['x-example'] = $validator->getList()[0];
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Specification;
namespace Appwrite\SDK\Specification;
class Specification
{
+39 -24
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Utopia;
use Appwrite\Auth\Auth;
use Appwrite\SDK\Method;
use Appwrite\Utopia\Request\Filter;
use Swoole\Http\Request as SwooleRequest;
use Utopia\Database\Validator\Authorization;
@@ -29,35 +30,49 @@ class Request extends UtopiaRequest
{
$parameters = parent::getParams();
if ($this->hasFilters() && self::hasRoute()) {
$methods = self::getRoute()->getLabel('sdk', null);
if (!$this->hasFilters() || !self::hasRoute()) {
return $parameters;
}
if (!\is_array($methods)) {
$methods = [$methods];
}
$methods = self::getRoute()->getLabel('sdk', null);
$methods = \array_filter($methods, function ($method) {
return !empty($method);
});
$params = [];
$endpointIdentifier = 'unknown.unknown';
foreach ($methods as $method) {
/** @var \Appwrite\SDK\Method $method */
$endpointIdentifier = $method->getNamespace() . '.' . $method->getMethodName();
$params += $method->getParameters();
}
if (!empty($params)) {
$parameters = array_filter($parameters, function ($key) use ($params) {
return array_key_exists($key, $params);
}, \ARRAY_FILTER_USE_KEY);
}
if (empty($methods)) {
return $parameters;
}
if (!\is_array($methods)) {
$id = $methods->getNamespace() . '.' . $methods->getMethodName();
foreach ($this->getFilters() as $filter) {
$parameters = $filter->parse($parameters, $endpointIdentifier);
$parameters = $filter->parse($parameters, $id);
}
return $parameters;
}
$matched = null;
foreach ($methods as $method) {
/** @var Method|null $method */
if ($method === null) {
continue;
}
// Find the method that matches the parameters passed
$methodParamNames = \array_map(fn ($param) => $param->getName(), $method->getParameters());
$invalidParams = \array_diff(\array_keys($parameters), $methodParamNames);
// No params defined, or all params are valid
if (empty($methodParamNames) || empty($invalidParams)) {
$matched = $method;
break;
}
}
$id = $matched !== null
? $matched->getNamespace() . '.' . $matched->getMethodName()
: 'unknown.unknown';
// Apply filters
foreach ($this->getFilters() as $filter) {
$parameters = $filter->parse($parameters, $id);
}
return $parameters;