From 29f92d9bd366690a795cb8e7547fffeb1b2656bb Mon Sep 17 00:00:00 2001 From: Treeed <21248276+Treeed@users.noreply.github.com> Date: Sat, 22 Feb 2025 17:29:14 +0100 Subject: [PATCH 01/18] Split attachment paths (#848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed attachment statistics for sqlite * Split attachment path into internal and external path, so the external source URL can be retained after a file is downloaded * Make internal and external path for attachments nullable, to make clear that they have no internal or external path * Added migrations for nullable columns for postgres and mysql * Added migration for nullable internal and external pathes for sqlite * Added translations * Fixed upload error * Restrict length of filename badge in attachment edit view * Improved margins with badges in attachment edit * Added a link to view external version from attachment edit * Let media_url stay in API attachments responses for backward compatibility --------- Co-authored-by: jona Co-authored-by: Jan Böhmer --- migrations/Version20250220215048.php | 87 +++++++ src/Controller/AttachmentFileController.php | 16 +- src/DataFixtures/PartFixtures.php | 2 +- src/DataTables/AttachmentDataTable.php | 72 +++--- src/Entity/Attachments/Attachment.php | 235 +++++++++++------- .../AttachmentDeleteListener.php | 4 +- src/Repository/AttachmentRepository.php | 18 +- src/Serializer/AttachmentNormalizer.php | 6 +- .../Attachments/AttachmentManager.php | 54 ++-- .../Attachments/AttachmentPathResolver.php | 8 +- .../Attachments/AttachmentReverseSearch.php | 2 +- .../Attachments/AttachmentSubmitHandler.php | 28 ++- .../Attachments/AttachmentURLGenerator.php | 18 +- .../Mergers/EntityMergerHelperTrait.php | 3 +- src/Services/EntityURLGenerator.php | 31 ++- .../PartKeeprImporter/PKImportHelperTrait.php | 2 +- .../LabelSystem/SandboxedTwigFactory.php | 4 +- .../parts/edit/edit_form_styles.html.twig | 48 ++-- .../parts/info/_attachments_info.html.twig | 27 +- .../API/Endpoints/AttachmentsEndpointTest.php | 2 +- tests/Entity/Attachments/AttachmentTest.php | 149 +++++------ translations/messages.en.xlf | 116 +++++---- 22 files changed, 561 insertions(+), 371 deletions(-) create mode 100644 migrations/Version20250220215048.php diff --git a/migrations/Version20250220215048.php b/migrations/Version20250220215048.php new file mode 100644 index 00000000..82e132de --- /dev/null +++ b/migrations/Version20250220215048.php @@ -0,0 +1,87 @@ +addSql('ALTER TABLE attachments ADD internal_path VARCHAR(255) DEFAULT NULL, ADD external_path VARCHAR(255) DEFAULT NULL'); + + //Copy the data from path to external_path and remove the path column + $this->addSql('UPDATE attachments SET external_path=path'); + $this->addSql('ALTER TABLE attachments DROP path'); + + + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('UPDATE attachments SET external_path=internal_path WHERE internal_path IS NOT NULL'); + $this->addSql('ALTER TABLE attachments DROP internal_path'); + $this->addSql('ALTER TABLE attachments RENAME COLUMN external_path TO path'); + } + + public function postgreSQLUp(Schema $schema): void + { + //We can use the same SQL for PostgreSQL as for MySQL + $this->mySQLUp($schema); + } + + public function postgreSQLDown(Schema $schema): void + { + //We can use the same SQL for PostgreSQL as for MySQL + $this->mySQLDown($schema); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__attachments AS SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, path FROM attachments'); + $this->addSql('DROP TABLE attachments'); + $this->addSql('CREATE TABLE attachments (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, type_id INTEGER NOT NULL, original_filename VARCHAR(255) DEFAULT NULL, show_in_table BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, class_name VARCHAR(255) NOT NULL, element_id INTEGER NOT NULL, internal_path VARCHAR(255) DEFAULT NULL, external_path VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_47C4FAD6C54C8C93 FOREIGN KEY (type_id) REFERENCES attachment_types (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO attachments (id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, external_path) SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, path FROM __temp__attachments'); + $this->addSql('DROP TABLE __temp__attachments'); + $this->addSql('CREATE INDEX attachment_element_idx ON attachments (class_name, element_id)'); + $this->addSql('CREATE INDEX attachment_name_idx ON attachments (name)'); + $this->addSql('CREATE INDEX attachments_idx_class_name_id ON attachments (class_name, id)'); + $this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON attachments (id, element_id, class_name)'); + $this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON attachments (type_id)'); + $this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON attachments (element_id)'); + + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL'); + } + + public function sqLiteDown(Schema $schema): void + { + //Reuse the MySQL down migration: + $this->mySQLDown($schema); + } + + +} diff --git a/src/Controller/AttachmentFileController.php b/src/Controller/AttachmentFileController.php index 936d27c5..d8bd8d87 100644 --- a/src/Controller/AttachmentFileController.php +++ b/src/Controller/AttachmentFileController.php @@ -51,15 +51,15 @@ class AttachmentFileController extends AbstractController $this->denyAccessUnlessGranted('show_private', $attachment); } - if ($attachment->isExternal()) { - throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!'); + if (!$attachment->hasInternal()) { + throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); } - if (!$helper->isFileExisting($attachment)) { + if (!$helper->isInternalFileExisting($attachment)) { throw $this->createNotFoundException('The file associated with the attachment is not existing!'); } - $file_path = $helper->toAbsoluteFilePath($attachment); + $file_path = $helper->toAbsoluteInternalFilePath($attachment); $response = new BinaryFileResponse($file_path); //Set header content disposition, so that the file will be downloaded @@ -80,15 +80,15 @@ class AttachmentFileController extends AbstractController $this->denyAccessUnlessGranted('show_private', $attachment); } - if ($attachment->isExternal()) { - throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!'); + if (!$attachment->hasInternal()) { + throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); } - if (!$helper->isFileExisting($attachment)) { + if (!$helper->isInternalFileExisting($attachment)) { throw $this->createNotFoundException('The file associated with the attachment is not existing!'); } - $file_path = $helper->toAbsoluteFilePath($attachment); + $file_path = $helper->toAbsoluteInternalFilePath($attachment); $response = new BinaryFileResponse($file_path); //Set header content disposition, so that the file will be downloaded diff --git a/src/DataFixtures/PartFixtures.php b/src/DataFixtures/PartFixtures.php index 0c8ea36d..a60d037d 100644 --- a/src/DataFixtures/PartFixtures.php +++ b/src/DataFixtures/PartFixtures.php @@ -131,7 +131,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface $attachment = new PartAttachment(); $attachment->setName('Test2'); - $attachment->setPath('invalid'); + $attachment->setInternalPath('invalid'); $attachment->setShowInTable(true); $attachment->setAttachmentType($manager->find(AttachmentType::class, 1)); $part->addAttachment($attachment); diff --git a/src/DataTables/AttachmentDataTable.php b/src/DataTables/AttachmentDataTable.php index 0d6c5b53..7973bf8a 100644 --- a/src/DataTables/AttachmentDataTable.php +++ b/src/DataTables/AttachmentDataTable.php @@ -50,8 +50,8 @@ final class AttachmentDataTable implements DataTableTypeInterface { $dataTable->add('dont_matter', RowClassColumn::class, [ 'render' => function ($value, Attachment $context): string { - //Mark attachments with missing files yellow - if(!$this->attachmentHelper->isFileExisting($context)){ + //Mark attachments yellow which have an internal file linked that doesn't exist + if($context->hasInternal() && !$this->attachmentHelper->isInternalFileExisting($context)){ return 'table-warning'; } @@ -64,8 +64,8 @@ final class AttachmentDataTable implements DataTableTypeInterface 'className' => 'no-colvis', 'render' => function ($value, Attachment $context): string { if ($context->isPicture() - && !$context->isExternal() - && $this->attachmentHelper->isFileExisting($context)) { + && $this->attachmentHelper->isInternalFileExisting($context)) { + $title = htmlspecialchars($context->getName()); if ($context->getFilename()) { $title .= ' ('.htmlspecialchars($context->getFilename()).')'; @@ -93,26 +93,6 @@ final class AttachmentDataTable implements DataTableTypeInterface $dataTable->add('name', TextColumn::class, [ 'label' => 'attachment.edit.name', 'orderField' => 'NATSORT(attachment.name)', - 'render' => function ($value, Attachment $context) { - //Link to external source - if ($context->isExternal()) { - return sprintf( - '%s', - htmlspecialchars((string) $context->getURL()), - htmlspecialchars($value) - ); - } - - if ($this->attachmentHelper->isFileExisting($context)) { - return sprintf( - '%s', - $this->entityURLGenerator->viewURL($context), - htmlspecialchars($value) - ); - } - - return $value; - }, ]); $dataTable->add('attachment_type', TextColumn::class, [ @@ -136,25 +116,57 @@ final class AttachmentDataTable implements DataTableTypeInterface ), ]); - $dataTable->add('filename', TextColumn::class, [ - 'label' => $this->translator->trans('attachment.table.filename'), + $dataTable->add('internal_link', TextColumn::class, [ + 'label' => 'attachment.table.internal_file', 'propertyPath' => 'filename', + 'render' => function ($value, Attachment $context) { + if ($this->attachmentHelper->isInternalFileExisting($context)) { + return sprintf( + '%s', + $this->entityURLGenerator->viewURL($context), + htmlspecialchars($value) + ); + } + + return $value; + } + ]); + + $dataTable->add('external_link', TextColumn::class, [ + 'label' => 'attachment.table.external_link', + 'propertyPath' => 'host', + 'render' => function ($value, Attachment $context) { + if ($context->hasExternal()) { + return sprintf( + '%s', + htmlspecialchars((string) $context->getExternalPath()), + htmlspecialchars($value) + ); + } + + return $value; + } ]); $dataTable->add('filesize', TextColumn::class, [ 'label' => $this->translator->trans('attachment.table.filesize'), 'render' => function ($value, Attachment $context) { - if ($context->isExternal()) { + if (!$context->hasInternal()) { return sprintf( ' %s ', - $this->translator->trans('attachment.external') + $this->translator->trans('attachment.external_only') ); } - if ($this->attachmentHelper->isFileExisting($context)) { - return $this->attachmentHelper->getHumanFileSize($context); + if ($this->attachmentHelper->isInternalFileExisting($context)) { + return sprintf( + ' + %s + ', + $this->attachmentHelper->getHumanFileSize($context) + ); } return sprintf( diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 30d9e257..3c8d890d 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -78,11 +78,16 @@ use LogicException; denormalizationContext: ['groups' => ['attachment:write', 'attachment:write:standalone', 'api:basic:write'], 'openapi_definition_name' => 'Write'], processor: HandleAttachmentsUploadsProcessor::class, )] -#[DocumentedAPIProperty(schemaName: 'Attachment-Read', property: 'media_url', type: 'string', nullable: true, - description: 'The URL to the file, where the attachment file can be downloaded. This can be an internal or external URL.', - example: '/media/part/2/bc547-6508afa5a79c8.pdf')] -#[DocumentedAPIProperty(schemaName: 'Attachment-Read', property: 'thumbnail_url', type: 'string', nullable: true, - description: 'The URL to a thumbnail version of this file. This only exists for internal picture attachments.')] +//This property is added by the denormalizer in order to resolve the placeholder +#[DocumentedAPIProperty( + schemaName: 'Attachment-Read', property: 'internal_path', type: 'string', nullable: false, + description: 'The URL to the internally saved copy of the file, if one exists', + example: '/media/part/2/bc547-6508afa5a79c8.pdf' +)] +#[DocumentedAPIProperty( + schemaName: 'Attachment-Read', property: 'thumbnail_url', type: 'string', nullable: true, + description: 'The URL to a thumbnail version of this file. This only exists for internal picture attachments.' +)] #[ApiFilter(LikeFilter::class, properties: ["name"])] #[ApiFilter(EntityFilter::class, properties: ["attachment_type"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] @@ -119,10 +124,6 @@ abstract class Attachment extends AbstractNamedDBElement */ final public const MODEL_EXTS = ['x3d']; - /** - * When the path begins with one of the placeholders. - */ - final public const INTERNAL_PLACEHOLDER = ['%BASE%', '%MEDIA%', '%SECURE%']; /** * @var array placeholders for attachments which using built in files @@ -152,10 +153,21 @@ abstract class Attachment extends AbstractNamedDBElement protected ?string $original_filename = null; /** - * @var string The path to the file relative to a placeholder path like %MEDIA% + * @var string|null If a copy of the file is stored internally, the path to the file relative to a placeholder + * path like %MEDIA% */ - #[ORM\Column(name: 'path', type: Types::STRING)] - protected string $path = ''; + #[ORM\Column(type: Types::STRING, nullable: true)] + protected ?string $internal_path = null; + + + /** + * @var string|null The path to the external source if the file is stored externally or was downloaded from an + * external source. Null if there is no external source. + */ + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Groups(['attachment:read'])] + #[ApiProperty(example: 'http://example.com/image.jpg')] + protected ?string $external_path = null; /** * @var string the name of this element @@ -237,7 +249,7 @@ abstract class Attachment extends AbstractNamedDBElement /** * Check if this attachment is a picture (analyse the file's extension). - * If the link is external, it is assumed that this is true. + * If the link is only external and doesn't contain an extension, it is assumed that this is true. * * @return bool * true if the file extension is a picture extension * * otherwise false @@ -245,54 +257,67 @@ abstract class Attachment extends AbstractNamedDBElement #[Groups(['attachment:read'])] public function isPicture(): bool { - if ($this->isExternal()) { + if($this->hasInternal()){ + + $extension = pathinfo($this->getInternalPath(), PATHINFO_EXTENSION); + + return in_array(strtolower($extension), static::PICTURE_EXTS, true); + + } + if ($this->hasExternal()) { //Check if we can extract a file extension from the URL - $extension = pathinfo(parse_url($this->path, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION); + $extension = pathinfo(parse_url($this->getExternalPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION); //If no extension is found or it is known picture extension, we assume that this is a picture extension return $extension === '' || in_array(strtolower($extension), static::PICTURE_EXTS, true); } - - $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION); - - return in_array(strtolower($extension), static::PICTURE_EXTS, true); + //File doesn't have an internal, nor an external copy. This shouldn't happen, but it certainly isn't a picture... + return false; } /** * Check if this attachment is a 3D model and therefore can be directly shown to user. - * If the attachment is external, false is returned (3D Models must be internal). + * If no internal copy exists, false is returned (3D Models must be internal). */ #[Groups(['attachment:read'])] #[SerializedName('3d_model')] public function is3DModel(): bool { //We just assume that 3D Models are internally saved, otherwise we get problems loading them. - if ($this->isExternal()) { + if (!$this->hasInternal()) { return false; } - $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION); + $extension = pathinfo($this->getInternalPath(), PATHINFO_EXTENSION); return in_array(strtolower($extension), static::MODEL_EXTS, true); } /** - * Checks if the attachment file is externally saved (the database saves an URL). + * Checks if this attachment has a path to an external file * - * @return bool true, if the file is saved externally + * @return bool true, if there is a path to an external file + * @phpstan-assert-if-true non-empty-string $this->external_path + * @phpstan-assert-if-true non-empty-string $this->getExternalPath()) */ #[Groups(['attachment:read'])] - public function isExternal(): bool + public function hasExternal(): bool { - //When path is empty, this attachment can not be external - if ($this->path === '') { - return false; - } + return $this->external_path !== null && $this->external_path !== ''; + } - //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode - $tmp = explode('/', $this->path); - - return !in_array($tmp[0], array_merge(static::INTERNAL_PLACEHOLDER, static::BUILTIN_PLACEHOLDER), true); + /** + * Checks if this attachment has a path to an internal file. + * Does not check if the file exists. + * + * @return bool true, if there is a path to an internal file + * @phpstan-assert-if-true non-empty-string $this->internal_path + * @phpstan-assert-if-true non-empty-string $this->getInternalPath()) + */ + #[Groups(['attachment:read'])] + public function hasInternal(): bool + { + return $this->internal_path !== null && $this->internal_path !== ''; } /** @@ -305,8 +330,12 @@ abstract class Attachment extends AbstractNamedDBElement #[SerializedName('private')] public function isSecure(): bool { + if ($this->internal_path === null) { + return false; + } + //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode - $tmp = explode('/', $this->path); + $tmp = explode('/', $this->internal_path); return '%SECURE%' === $tmp[0]; } @@ -320,7 +349,11 @@ abstract class Attachment extends AbstractNamedDBElement #[Groups(['attachment:read'])] public function isBuiltIn(): bool { - return static::checkIfBuiltin($this->path); + if ($this->internal_path === null) { + return false; + } + + return static::checkIfBuiltin($this->internal_path); } /******************************************************************************** @@ -332,13 +365,13 @@ abstract class Attachment extends AbstractNamedDBElement /** * Returns the extension of the file referenced via the attachment. * For a path like %BASE/path/foo.bar, bar will be returned. - * If this attachment is external null is returned. + * If this attachment is only external null is returned. * * @return string|null the file extension in lower case */ public function getExtension(): ?string { - if ($this->isExternal()) { + if (!$this->hasInternal()) { return null; } @@ -346,7 +379,7 @@ abstract class Attachment extends AbstractNamedDBElement return strtolower(pathinfo($this->original_filename, PATHINFO_EXTENSION)); } - return strtolower(pathinfo($this->getPath(), PATHINFO_EXTENSION)); + return strtolower(pathinfo($this->getInternalPath(), PATHINFO_EXTENSION)); } /** @@ -361,52 +394,54 @@ abstract class Attachment extends AbstractNamedDBElement } /** - * The URL to the external file, or the path to the built-in file. + * The URL to the external file, or the path to the built-in file, but not paths to uploaded files. * Returns null, if the file is not external (and not builtin). + * The output of this function is such, that no changes occur when it is fed back into setURL(). + * Required for the Attachment form field. */ - #[Groups(['attachment:read'])] - #[SerializedName('url')] public function getURL(): ?string { - if (!$this->isExternal() && !$this->isBuiltIn()) { - return null; + if($this->hasExternal()){ + return $this->getExternalPath(); } - - return $this->path; + if($this->isBuiltIn()){ + return $this->getInternalPath(); + } + return null; } /** * Returns the hostname where the external file is stored. - * Returns null, if the file is not external. + * Returns null, if there is no external path. */ public function getHost(): ?string { - if (!$this->isExternal()) { + if (!$this->hasExternal()) { return null; } - return parse_url((string) $this->getURL(), PHP_URL_HOST); + return parse_url($this->getExternalPath(), PHP_URL_HOST); } - /** - * Get the filepath, relative to %BASE%. - * - * @return string A string like %BASE/path/foo.bar - */ - public function getPath(): string + public function getInternalPath(): ?string { - return $this->path; + return $this->internal_path; + } + + public function getExternalPath(): ?string + { + return $this->external_path; } /** * Returns the filename of the attachment. * For a path like %BASE/path/foo.bar, foo.bar will be returned. * - * If the path is a URL (can be checked via isExternal()), null will be returned. + * If there is no internal copy of the file, null will be returned. */ public function getFilename(): ?string { - if ($this->isExternal()) { + if (!$this->hasInternal()) { return null; } @@ -415,7 +450,7 @@ abstract class Attachment extends AbstractNamedDBElement return $this->original_filename; } - return pathinfo($this->getPath(), PATHINFO_BASENAME); + return pathinfo($this->getInternalPath(), PATHINFO_BASENAME); } /** @@ -488,15 +523,12 @@ abstract class Attachment extends AbstractNamedDBElement } /** - * Sets the filepath (with relative placeholder) for this attachment. - * - * @param string $path the new filepath of the attachment - * - * @return Attachment + * Sets the path to a file hosted internally. If you set this path to a file that was not downloaded from the + * external source in external_path, make sure to reset external_path. */ - public function setPath(string $path): self + public function setInternalPath(?string $internal_path): self { - $this->path = $path; + $this->internal_path = $internal_path; return $this; } @@ -512,34 +544,60 @@ abstract class Attachment extends AbstractNamedDBElement } /** - * Sets the url associated with this attachment. - * If the url is empty nothing is changed, to not override the file path. - * - * @return Attachment + * Sets up the paths using a user provided string which might contain an external path or a builtin path. Allows + * resetting the external path if an internal path exists. Resets any other paths if a (nonempty) new path is set. */ #[Groups(['attachment:write'])] #[SerializedName('url')] + #[ApiProperty(description: 'Set the path of the attachment here. + Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty + string if the attachment has an internal file associated and you\'d like to reset the external source. + If you set a new (nonempty) file path any associated internal file will be removed!')] public function setURL(?string $url): self { - //Do nothing if the URL is empty - if ($url === null || $url === '') { + //Don't allow the user to set an empty external path if the internal path is empty already + if (($url === null || $url === "") && !$this->hasInternal()) { return $this; } - $url = trim($url); - //Escape spaces in URL - $url = str_replace(' ', '%20', $url); - - //Only set if the URL is not empty - if ($url !== '') { - if (str_contains($url, '%BASE%') || str_contains($url, '%MEDIA%')) { - throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!'); - } - - $this->path = $url; - //Reset internal filename - $this->original_filename = null; + //The URL field can also contain the special builtin internal paths, so we need to distinguish here + if ($this::checkIfBuiltin($url)) { + $this->setInternalPath($url); + //make sure the external path isn't still pointing to something unrelated + $this->setExternalPath(null); + } else { + $this->setExternalPath($url); } + return $this; + } + + + /** + * Sets the path to a file hosted on an external server. Setting the external path to a (nonempty) value different + * from the the old one _clears_ the internal path, so that the external path reflects where any associated internal + * file came from. + */ + public function setExternalPath(?string $external_path): self + { + //If we only clear the external path, don't reset the internal path, since that could be confusing + if($external_path === null || $external_path === '') { + $this->external_path = null; + return $this; + } + + $external_path = trim($external_path); + //Escape spaces in URL + $external_path = str_replace(' ', '%20', $external_path); + + if($this->external_path === $external_path) { + //Nothing changed, nothing to do + return $this; + } + + $this->external_path = $external_path; + $this->internal_path = null; + //Reset internal filename + $this->original_filename = null; return $this; } @@ -551,12 +609,17 @@ abstract class Attachment extends AbstractNamedDBElement /** * Checks if the given path is a path to a builtin resource. * - * @param string $path The path that should be checked + * @param string|null $path The path that should be checked * * @return bool true if the path is pointing to a builtin resource */ - public static function checkIfBuiltin(string $path): bool + public static function checkIfBuiltin(?string $path): bool { + //An empty path can't be a builtin + if ($path === null) { + return false; + } + //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode $tmp = explode('/', $path); //Builtins must have a %PLACEHOLDER% construction diff --git a/src/EntityListeners/AttachmentDeleteListener.php b/src/EntityListeners/AttachmentDeleteListener.php index e9df5972..1f39b2d0 100644 --- a/src/EntityListeners/AttachmentDeleteListener.php +++ b/src/EntityListeners/AttachmentDeleteListener.php @@ -52,8 +52,8 @@ class AttachmentDeleteListener #[PreUpdate] public function preUpdateHandler(Attachment $attachment, PreUpdateEventArgs $event): void { - if ($event->hasChangedField('path')) { - $old_path = $event->getOldValue('path'); + if ($event->hasChangedField('internal_path')) { + $old_path = $event->getOldValue('internal_path'); //Dont delete file if the attachment uses a builtin ressource: if (Attachment::checkIfBuiltin($old_path)) { diff --git a/src/Repository/AttachmentRepository.php b/src/Repository/AttachmentRepository.php index 865443d2..0a6b1db2 100644 --- a/src/Repository/AttachmentRepository.php +++ b/src/Repository/AttachmentRepository.php @@ -58,7 +58,7 @@ class AttachmentRepository extends DBElementRepository { $qb = $this->createQueryBuilder('attachment'); $qb->select('COUNT(attachment)') - ->where('attachment.path LIKE :like ESCAPE \'#\''); + ->where('attachment.internal_path LIKE :like ESCAPE \'#\''); $qb->setParameter('like', '#%SECURE#%%'); $query = $qb->getQuery(); @@ -66,7 +66,7 @@ class AttachmentRepository extends DBElementRepository } /** - * Gets the count of all external attachments (attachments only containing a URL). + * Gets the count of all external attachments (attachments containing an external path). * * @throws NoResultException * @throws NonUniqueResultException @@ -75,17 +75,15 @@ class AttachmentRepository extends DBElementRepository { $qb = $this->createQueryBuilder('attachment'); $qb->select('COUNT(attachment)') - ->where('ILIKE(attachment.path, :http) = TRUE') - ->orWhere('ILIKE(attachment.path, :https) = TRUE'); - $qb->setParameter('http', 'http://%'); - $qb->setParameter('https', 'https://%'); + ->andWhere('attaachment.internal_path IS NULL') + ->where('attachment.external_path IS NOT NULL'); $query = $qb->getQuery(); return (int) $query->getSingleScalarResult(); } /** - * Gets the count of all attachments where a user uploaded a file. + * Gets the count of all attachments where a user uploaded a file or a file was downloaded from an external source. * * @throws NoResultException * @throws NonUniqueResultException @@ -94,9 +92,9 @@ class AttachmentRepository extends DBElementRepository { $qb = $this->createQueryBuilder('attachment'); $qb->select('COUNT(attachment)') - ->where('attachment.path LIKE :base ESCAPE \'#\'') - ->orWhere('attachment.path LIKE :media ESCAPE \'#\'') - ->orWhere('attachment.path LIKE :secure ESCAPE \'#\''); + ->where('attachment.internal_path LIKE :base ESCAPE \'#\'') + ->orWhere('attachment.internal_path LIKE :media ESCAPE \'#\'') + ->orWhere('attachment.internal_path LIKE :secure ESCAPE \'#\''); $qb->setParameter('secure', '#%SECURE#%%'); $qb->setParameter('base', '#%BASE#%%'); $qb->setParameter('media', '#%MEDIA#%%'); diff --git a/src/Serializer/AttachmentNormalizer.php b/src/Serializer/AttachmentNormalizer.php index 8a37b3c0..bd791d04 100644 --- a/src/Serializer/AttachmentNormalizer.php +++ b/src/Serializer/AttachmentNormalizer.php @@ -52,11 +52,15 @@ class AttachmentNormalizer implements NormalizerInterface, NormalizerAwareInterf $context[self::ALREADY_CALLED] = true; $data = $this->normalizer->normalize($object, $format, $context); + $data['internal_path'] = $this->attachmentURLGenerator->getInternalViewURL($object); - $data['media_url'] = $this->attachmentURLGenerator->getViewURL($object); //Add thumbnail url if the attachment is a picture $data['thumbnail_url'] = $object->isPicture() ? $this->attachmentURLGenerator->getThumbnailURL($object) : null; + //For backwards compatibility reasons + //Deprecated: Use internal_path and external_path instead + $data['media_url'] = $data['internal_path'] ?? $object->getExternalPath(); + return $data; } diff --git a/src/Services/Attachments/AttachmentManager.php b/src/Services/Attachments/AttachmentManager.php index 4429179e..1075141b 100644 --- a/src/Services/Attachments/AttachmentManager.php +++ b/src/Services/Attachments/AttachmentManager.php @@ -44,35 +44,31 @@ class AttachmentManager * * @param Attachment $attachment The attachment for which the file should be generated * - * @return SplFileInfo|null The fileinfo for the attachment file. Null, if the attachment is external or has + * @return SplFileInfo|null The fileinfo for the attachment file. Null, if the attachment is only external or has * invalid file. */ public function attachmentToFile(Attachment $attachment): ?SplFileInfo { - if ($attachment->isExternal() || !$this->isFileExisting($attachment)) { + if (!$this->isInternalFileExisting($attachment)) { return null; } - return new SplFileInfo($this->toAbsoluteFilePath($attachment)); + return new SplFileInfo($this->toAbsoluteInternalFilePath($attachment)); } /** - * Returns the absolute filepath of the attachment. Null is returned, if the attachment is externally saved, - * or is not existing. + * Returns the absolute filepath to the internal copy of the attachment. Null is returned, if the attachment is + * only externally saved, or is not existing. * * @param Attachment $attachment The attachment for which the filepath should be determined */ - public function toAbsoluteFilePath(Attachment $attachment): ?string + public function toAbsoluteInternalFilePath(Attachment $attachment): ?string { - if ($attachment->getPath() === '') { + if (!$attachment->hasInternal()){ return null; } - if ($attachment->isExternal()) { - return null; - } - - $path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); + $path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath()); //realpath does not work with null as argument if (null === $path) { @@ -89,8 +85,8 @@ class AttachmentManager } /** - * Checks if the file in this attachement is existing. This works for files on the HDD, and for URLs - * (it's not checked if the ressource behind the URL is really existing, so for every external attachment true is returned). + * Checks if the file in this attachment is existing. This works for files on the HDD, and for URLs + * (it's not checked if the resource behind the URL is really existing, so for every external attachment true is returned). * * @param Attachment $attachment The attachment for which the existence should be checked * @@ -98,15 +94,23 @@ class AttachmentManager */ public function isFileExisting(Attachment $attachment): bool { - if ($attachment->getPath() === '') { - return false; - } - - if ($attachment->isExternal()) { + if($attachment->hasExternal()){ return true; } + return $this->isInternalFileExisting($attachment); + } - $absolute_path = $this->toAbsoluteFilePath($attachment); + /** + * Checks if the internal file in this attachment is existing. Returns false if the attachment doesn't have an + * internal file. + * + * @param Attachment $attachment The attachment for which the existence should be checked + * + * @return bool true if the file is existing + */ + public function isInternalFileExisting(Attachment $attachment): bool + { + $absolute_path = $this->toAbsoluteInternalFilePath($attachment); if (null === $absolute_path) { return false; @@ -117,21 +121,17 @@ class AttachmentManager /** * Returns the filesize of the attachments in bytes. - * For external attachments or not existing attachments, null is returned. + * For purely external attachments or inexistent attachments, null is returned. * * @param Attachment $attachment the filesize for which the filesize should be calculated */ public function getFileSize(Attachment $attachment): ?int { - if ($attachment->isExternal()) { + if (!$this->isInternalFileExisting($attachment)) { return null; } - if (!$this->isFileExisting($attachment)) { - return null; - } - - $tmp = filesize($this->toAbsoluteFilePath($attachment)); + $tmp = filesize($this->toAbsoluteInternalFilePath($attachment)); return false !== $tmp ? $tmp : null; } diff --git a/src/Services/Attachments/AttachmentPathResolver.php b/src/Services/Attachments/AttachmentPathResolver.php index e3e7a3ca..1b52c89b 100644 --- a/src/Services/Attachments/AttachmentPathResolver.php +++ b/src/Services/Attachments/AttachmentPathResolver.php @@ -115,12 +115,16 @@ class AttachmentPathResolver * Converts an relative placeholder filepath (with %MEDIA% or older %BASE%) to an absolute filepath on disk. * The directory separator is always /. Relative pathes are not realy possible (.. is striped). * - * @param string $placeholder_path the filepath with placeholder for which the real path should be determined + * @param string|null $placeholder_path the filepath with placeholder for which the real path should be determined * * @return string|null The absolute real path of the file, or null if the placeholder path is invalid */ - public function placeholderToRealPath(string $placeholder_path): ?string + public function placeholderToRealPath(?string $placeholder_path): ?string { + if (null === $placeholder_path) { + return null; + } + //The new attachments use %MEDIA% as placeholders, which is the directory set in media_directory //Older path entries are given via %BASE% which was the project root diff --git a/src/Services/Attachments/AttachmentReverseSearch.php b/src/Services/Attachments/AttachmentReverseSearch.php index 5f4f86de..e05192d0 100644 --- a/src/Services/Attachments/AttachmentReverseSearch.php +++ b/src/Services/Attachments/AttachmentReverseSearch.php @@ -55,7 +55,7 @@ class AttachmentReverseSearch $repo = $this->em->getRepository(Attachment::class); return $repo->findBy([ - 'path' => [$relative_path_new, $relative_path_old], + 'internal_path' => [$relative_path_new, $relative_path_old], ]); } diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index d9b2a380..fc54dd2f 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -207,7 +207,7 @@ class AttachmentSubmitHandler if ($file instanceof UploadedFile) { $this->upload($attachment, $file, $secure_attachment); - } elseif ($upload->downloadUrl && $attachment->isExternal()) { + } elseif ($upload->downloadUrl && $attachment->hasExternal()) { $this->downloadURL($attachment, $secure_attachment); } @@ -244,12 +244,12 @@ class AttachmentSubmitHandler protected function renameBlacklistedExtensions(Attachment $attachment): Attachment { //We can not do anything on builtins or external ressources - if ($attachment->isBuiltIn() || $attachment->isExternal()) { + if ($attachment->isBuiltIn() || !$attachment->hasInternal()) { return $attachment; } //Determine the old filepath - $old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); + $old_path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath()); if ($old_path === null || $old_path === '' || !file_exists($old_path)) { return $attachment; } @@ -267,7 +267,7 @@ class AttachmentSubmitHandler $fs->rename($old_path, $new_path); //Update the attachment - $attachment->setPath($this->pathResolver->realPathToPlaceholder($new_path)); + $attachment->setInternalPath($this->pathResolver->realPathToPlaceholder($new_path)); } @@ -275,17 +275,17 @@ class AttachmentSubmitHandler } /** - * Move the given attachment to secure location (or back to public folder) if needed. + * Move the internal copy of the given attachment to a secure location (or back to public folder) if needed. * * @param Attachment $attachment the attachment for which the file should be moved * @param bool $secure_location this value determines, if the attachment is moved to the secure or public folder * - * @return Attachment The attachment with the updated filepath + * @return Attachment The attachment with the updated internal filepath */ protected function moveFile(Attachment $attachment, bool $secure_location): Attachment { //We can not do anything on builtins or external ressources - if ($attachment->isBuiltIn() || $attachment->isExternal()) { + if ($attachment->isBuiltIn() || !$attachment->hasInternal()) { return $attachment; } @@ -295,7 +295,7 @@ class AttachmentSubmitHandler } //Determine the old filepath - $old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); + $old_path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath()); if (!file_exists($old_path)) { return $attachment; } @@ -319,7 +319,7 @@ class AttachmentSubmitHandler //Save info to attachment entity $new_path = $this->pathResolver->realPathToPlaceholder($new_path); - $attachment->setPath($new_path); + $attachment->setInternalPath($new_path); return $attachment; } @@ -329,7 +329,7 @@ class AttachmentSubmitHandler * * @param bool $secureAttachment True if the file should be moved to the secure attachment storage * - * @return Attachment The attachment with the new filepath + * @return Attachment The attachment with the downloaded copy */ protected function downloadURL(Attachment $attachment, bool $secureAttachment): Attachment { @@ -338,7 +338,7 @@ class AttachmentSubmitHandler throw new RuntimeException('Download of attachments is not allowed!'); } - $url = $attachment->getURL(); + $url = $attachment->getExternalPath(); $fs = new Filesystem(); $attachment_folder = $this->generateAttachmentPath($attachment, $secureAttachment); @@ -399,7 +399,7 @@ class AttachmentSubmitHandler //Make our file path relative to %BASE% $new_path = $this->pathResolver->realPathToPlaceholder($new_path); //Save the path to the attachment - $attachment->setPath($new_path); + $attachment->setInternalPath($new_path); } catch (TransportExceptionInterface) { throw new AttachmentDownloadException('Transport error!'); } @@ -427,7 +427,9 @@ class AttachmentSubmitHandler //Make our file path relative to %BASE% $file_path = $this->pathResolver->realPathToPlaceholder($file_path); //Save the path to the attachment - $attachment->setPath($file_path); + $attachment->setInternalPath($file_path); + //reset any external paths the attachment might have had + $attachment->setExternalPath(null); //And save original filename $attachment->setFilename($file->getClientOriginalName()); diff --git a/src/Services/Attachments/AttachmentURLGenerator.php b/src/Services/Attachments/AttachmentURLGenerator.php index d28a8d65..c22cefe4 100644 --- a/src/Services/Attachments/AttachmentURLGenerator.php +++ b/src/Services/Attachments/AttachmentURLGenerator.php @@ -92,9 +92,9 @@ class AttachmentURLGenerator * Returns a URL under which the attachment file can be viewed. * @return string|null The URL or null if the attachment file is not existing */ - public function getViewURL(Attachment $attachment): ?string + public function getInternalViewURL(Attachment $attachment): ?string { - $absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment); + $absolute_path = $this->attachmentHelper->toAbsoluteInternalFilePath($attachment); if (null === $absolute_path) { return null; } @@ -111,6 +111,7 @@ class AttachmentURLGenerator /** * Returns a URL to a thumbnail of the attachment file. + * For external files the original URL is returned. * @return string|null The URL or null if the attachment file is not existing */ public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string @@ -119,11 +120,14 @@ class AttachmentURLGenerator throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!'); } - if ($attachment->isExternal() && ($attachment->getURL() !== null && $attachment->getURL() !== '')) { - return $attachment->getURL(); + if (!$attachment->hasInternal()){ + if($attachment->hasExternal()) { + return $attachment->getExternalPath(); + } + return null; } - $absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment); + $absolute_path = $this->attachmentHelper->toAbsoluteInternalFilePath($attachment); if (null === $absolute_path) { return null; } @@ -137,7 +141,7 @@ class AttachmentURLGenerator //GD can not work with SVG, so serve it directly... //We can not use getExtension here, because it uses the original filename and not the real extension //Instead we use the logic, which is also used to determine if the attachment is a picture - $extension = pathinfo(parse_url($attachment->getPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION); + $extension = pathinfo(parse_url($attachment->getInternalPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION); if ('svg' === $extension) { return $this->assets->getUrl($asset_path); } @@ -157,7 +161,7 @@ class AttachmentURLGenerator /** * Returns a download link to the file associated with the attachment. */ - public function getDownloadURL(Attachment $attachment): string + public function getInternalDownloadURL(Attachment $attachment): string { //Redirect always to download controller, which sets the correct headers for downloading: return $this->urlGenerator->generate('attachment_download', ['id' => $attachment->getID()]); diff --git a/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php b/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php index a5c9a5fa..64c952a9 100644 --- a/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php +++ b/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php @@ -247,7 +247,8 @@ trait EntityMergerHelperTrait { return $this->mergeCollections($target, $other, 'attachments', fn(Attachment $t, Attachment $o): bool => $t->getName() === $o->getName() && $t->getAttachmentType() === $o->getAttachmentType() - && $t->getPath() === $o->getPath()); + && $t->getExternalPath() === $o->getExternalPath() + && $t->getInternalPath() === $o->getInternalPath()); } /** diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 5718daec..c66b5fd9 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -156,25 +156,32 @@ class EntityURLGenerator public function viewURL(Attachment $entity): string { - if ($entity->isExternal()) { //For external attachments, return the link to external path - return $entity->getURL() ?? throw new \RuntimeException('External attachment has no URL!'); + if ($entity->hasInternal()) { + return $this->attachmentURLGenerator->getInternalViewURL($entity); } - //return $this->urlGenerator->generate('attachment_view', ['id' => $entity->getID()]); - return $this->attachmentURLGenerator->getViewURL($entity) ?? ''; + + if($entity->hasExternal()) { + return $entity->getExternalPath(); + } + + throw new \RuntimeException('Attachment has no internal nor external path!'); } public function downloadURL($entity): string { - if ($entity instanceof Attachment) { - if ($entity->isExternal()) { //For external attachments, return the link to external path - return $entity->getURL() ?? throw new \RuntimeException('External attachment has no URL!'); - } - - return $this->attachmentURLGenerator->getDownloadURL($entity); + if (!($entity instanceof Attachment)) { + throw new EntityNotSupportedException(sprintf('The given entity is not supported yet! Passed class type: %s', $entity::class)); } - //Otherwise throw an error - throw new EntityNotSupportedException(sprintf('The given entity is not supported yet! Passed class type: %s', $entity::class)); + if ($entity->hasInternal()) { + return $this->attachmentURLGenerator->getInternalDownloadURL($entity); + } + + if($entity->hasExternal()) { + return $entity->getExternalPath(); + } + + throw new \RuntimeException('Attachment has not internal or external path!'); } /** diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php index 5489fc8c..1e4cd3ba 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php @@ -105,7 +105,7 @@ trait PKImportHelperTrait //Next comes the filename plus extension $path .= '/'.$attachment_row['filename'].'.'.$attachment_row['extension']; - $attachment->setPath($path); + $attachment->setInternalPath($path); return $attachment; } diff --git a/src/Services/LabelSystem/SandboxedTwigFactory.php b/src/Services/LabelSystem/SandboxedTwigFactory.php index cdf0594f..d6ea6968 100644 --- a/src/Services/LabelSystem/SandboxedTwigFactory.php +++ b/src/Services/LabelSystem/SandboxedTwigFactory.php @@ -122,8 +122,8 @@ final class SandboxedTwigFactory 'getFullPath', 'getPathArray', 'getSubelements', 'getChildren', 'isNotSelectable', ], AbstractCompany::class => ['getAddress', 'getPhoneNumber', 'getFaxNumber', 'getEmailAddress', 'getWebsite', 'getAutoProductUrl'], AttachmentContainingDBElement::class => ['getAttachments', 'getMasterPictureAttachment'], - Attachment::class => ['isPicture', 'is3DModel', 'isExternal', 'isSecure', 'isBuiltIn', 'getExtension', - 'getElement', 'getURL', 'getHost', 'getFilename', 'getAttachmentType', 'getShowInTable', ], + Attachment::class => ['isPicture', 'is3DModel', 'hasExternal', 'hasInternal', 'isSecure', 'isBuiltIn', 'getExtension', + 'getElement', 'getExternalPath', 'getHost', 'getFilename', 'getAttachmentType', 'getShowInTable'], AbstractParameter::class => ['getFormattedValue', 'getGroup', 'getSymbol', 'getValueMin', 'getValueMax', 'getValueTypical', 'getUnit', 'getValueText', ], MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'], diff --git a/templates/parts/edit/edit_form_styles.html.twig b/templates/parts/edit/edit_form_styles.html.twig index cf9a40b9..062122de 100644 --- a/templates/parts/edit/edit_form_styles.html.twig +++ b/templates/parts/edit/edit_form_styles.html.twig @@ -152,35 +152,32 @@ {% set attach = form.vars.value %} + {# @var \App\Entity\Attachments\Attachment attach #} {% if attach is not null %} - {% if attachment_manager.fileExisting(attach) %} - {% if not attach.external %} -

-
+ {% if not attach.hasInternal() and attach.external %} +
- {{ attach.filename }} + {% trans %}attachment.external_only{% endtrans %} -
- +
+ {% elseif attachment_manager.isInternalFileExisting(attach) %} +
+
+ {{ attach.filename|u.truncate(25, ' ...') }} +
+
+
{{ attachment_manager.humanFileSize(attach) }} - -
- {% else %} -

-
- - {% trans %}attachment.external{% endtrans %} - -
- {% endif %} + + {% if attach.secure %} -
+
{% trans %}attachment.secure{% endtrans %} -
+ {% endif %} {% if attach.secure and not is_granted('show_private', attach) %} @@ -190,16 +187,21 @@ {% trans %}attachment.preview.alt{% endtrans %} {% else %} - {% trans %}attachment.view{% endtrans %} + {% trans %}attachment.view_local{% endtrans %} {% endif %} {% else %} -

-
+
{% trans %}attachment.file_not_found{% endtrans %} -
+ + {% endif %} + {% if attach.external %} +
+ {% trans %}attachment.view_external{% endtrans %} +
{% endif %} {% endif %} diff --git a/templates/parts/info/_attachments_info.html.twig b/templates/parts/info/_attachments_info.html.twig index 995c69eb..4f7c0455 100644 --- a/templates/parts/info/_attachments_info.html.twig +++ b/templates/parts/info/_attachments_info.html.twig @@ -24,18 +24,16 @@ {{ attachment.name }} {{ attachment.attachmentType.fullPath }} - {% if attachment.external %} - {{ attachment.host }} - {% else %} + {% if attachment.hasInternal() %} {{ attachment.filename }} {% endif %} - {% if attachment.external %} + {% if not attachment.hasInternal() %} - {% trans %}attachment.external{% endtrans %} + {% trans %}attachment.external_only{% endtrans %} - {% elseif attachment_manager.fileExisting(attachment) %} + {% elseif attachment_manager.internalFileExisting(attachment) %} {{ attachment_manager.humanFileSize(attachment) }} @@ -58,14 +56,19 @@
- + + + + - + diff --git a/tests/API/Endpoints/AttachmentsEndpointTest.php b/tests/API/Endpoints/AttachmentsEndpointTest.php index 8084db5c..8f4d7e77 100644 --- a/tests/API/Endpoints/AttachmentsEndpointTest.php +++ b/tests/API/Endpoints/AttachmentsEndpointTest.php @@ -91,7 +91,7 @@ class AttachmentsEndpointTest extends AuthenticatedApiTestCase //Attachment must be set (not null) $array = json_decode($response->getContent(), true); - self::assertNotNull($array['media_url']); + self::assertNotNull($array['internal_path']); //Attachment must be private self::assertJsonContains([ diff --git a/tests/Entity/Attachments/AttachmentTest.php b/tests/Entity/Attachments/AttachmentTest.php index a2179e53..bac28fd4 100644 --- a/tests/Entity/Attachments/AttachmentTest.php +++ b/tests/Entity/Attachments/AttachmentTest.php @@ -59,14 +59,15 @@ class AttachmentTest extends TestCase $this->assertNull($attachment->getAttachmentType()); $this->assertFalse($attachment->isPicture()); - $this->assertFalse($attachment->isExternal()); + $this->assertFalse($attachment->hasExternal()); + $this->assertFalse($attachment->hasInternal()); $this->assertFalse($attachment->isSecure()); $this->assertFalse($attachment->isBuiltIn()); $this->assertFalse($attachment->is3DModel()); $this->assertFalse($attachment->getShowInTable()); - $this->assertEmpty($attachment->getPath()); + $this->assertEmpty($attachment->getInternalPath()); + $this->assertEmpty($attachment->getExternalPath()); $this->assertEmpty($attachment->getName()); - $this->assertEmpty($attachment->getURL()); $this->assertEmpty($attachment->getExtension()); $this->assertNull($attachment->getElement()); $this->assertEmpty($attachment->getFilename()); @@ -119,82 +120,63 @@ class AttachmentTest extends TestCase $attachment->setElement($element); } - public function externalDataProvider(): \Iterator - { - yield ['', false]; - yield ['%MEDIA%/foo/bar.txt', false]; - yield ['%BASE%/foo/bar.jpg', false]; - yield ['%FOOTPRINTS%/foo/bar.jpg', false]; - yield ['%FOOTPRINTS3D%/foo/bar.jpg', false]; - yield ['%SECURE%/test.txt', false]; - yield ['%test%/foo/bar.ghp', true]; - yield ['foo%MEDIA%/foo.jpg', true]; - yield ['foo%MEDIA%/%BASE%foo.jpg', true]; - } - /** - * @dataProvider externalDataProvider - */ - public function testIsExternal($path, $expected): void + public static function extensionDataProvider(): \Iterator { - $attachment = new PartAttachment(); - $this->setProtectedProperty($attachment, 'path', $path); - $this->assertSame($expected, $attachment->isExternal()); - } - - public function extensionDataProvider(): \Iterator - { - yield ['%MEDIA%/foo/bar.txt', null, 'txt']; - yield ['%MEDIA%/foo/bar.JPeg', null, 'jpeg']; - yield ['%MEDIA%/foo/bar.JPeg', 'test.txt', 'txt']; - yield ['%MEDIA%/foo/bar', null, '']; - yield ['%MEDIA%/foo.bar', 'bar', '']; - yield ['http://google.de', null, null]; - yield ['https://foo.bar', null, null]; - yield ['https://foo.bar/test.jpeg', null, null]; - yield ['test', null, null]; - yield ['test.txt', null, null]; + yield ['%MEDIA%/foo/bar.txt', 'http://google.de', null, 'txt']; + yield ['%MEDIA%/foo/bar.JPeg', 'https://foo.bar', null, 'jpeg']; + yield ['%MEDIA%/foo/bar.JPeg', null, 'test.txt', 'txt']; + yield ['%MEDIA%/foo/bar', 'https://foo.bar/test.jpeg', null, '']; + yield ['%MEDIA%/foo.bar', 'test.txt', 'bar', '']; + yield [null, 'http://google.de', null, null]; + yield [null, 'https://foo.bar', null, null]; + yield [null, ',https://foo.bar/test.jpeg', null, null]; + yield [null, 'test', null, null]; + yield [null, 'test.txt', null, null]; } /** * @dataProvider extensionDataProvider */ - public function testGetExtension($path, $originalFilename, $expected): void + public function testGetExtension(?string $internal_path, ?string $external_path, ?string $originalFilename, ?string $expected): void { $attachment = new PartAttachment(); - $this->setProtectedProperty($attachment, 'path', $path); + $this->setProtectedProperty($attachment, 'internal_path', $internal_path); + $this->setProtectedProperty($attachment, 'external_path', $external_path); $this->setProtectedProperty($attachment, 'original_filename', $originalFilename); $this->assertSame($expected, $attachment->getExtension()); } - public function pictureDataProvider(): \Iterator + public static function pictureDataProvider(): \Iterator { - yield ['%MEDIA%/foo/bar.txt', false]; - yield ['https://test.de/picture.jpeg', true]; - yield ['https://test.de/picture.png?test=fdsj&width=34', true]; - yield ['https://invalid.invalid/file.txt', false]; - yield ['http://infsf.inda/file.zip?test', false]; - yield ['https://test.de', true]; - yield ['https://invalid.com/invalid/pic', true]; - yield ['%MEDIA%/foo/bar.jpeg', true]; - yield ['%MEDIA%/foo/bar.webp', true]; - yield ['%MEDIA%/foo', false]; - yield ['%SECURE%/foo.txt/test', false]; + yield [null, '%MEDIA%/foo/bar.txt', false]; + yield [null, 'https://test.de/picture.jpeg', true]; + yield [null, 'https://test.de/picture.png?test=fdsj&width=34', true]; + yield [null, 'https://invalid.invalid/file.txt', false]; + yield [null, 'http://infsf.inda/file.zip?test', false]; + yield [null, 'https://test.de', true]; + yield [null, 'https://invalid.com/invalid/pic', true]; + yield ['%MEDIA%/foo/bar.jpeg', 'https://invalid.invalid/file.txt', true]; + yield ['%MEDIA%/foo/bar.webp', '', true]; + yield ['%MEDIA%/foo', '', false]; + yield ['%SECURE%/foo.txt/test', 'https://test.de/picture.jpeg', false]; } /** * @dataProvider pictureDataProvider */ - public function testIsPicture($path, $expected): void + public function testIsPicture(?string $internal_path, ?string $external_path, bool $expected): void { $attachment = new PartAttachment(); - $this->setProtectedProperty($attachment, 'path', $path); + $this->setProtectedProperty($attachment, 'internal_path', $internal_path); + $this->setProtectedProperty($attachment, 'external_path', $external_path); $this->assertSame($expected, $attachment->isPicture()); } - public function builtinDataProvider(): \Iterator + public static function builtinDataProvider(): \Iterator { yield ['', false]; + yield [null, false]; yield ['%MEDIA%/foo/bar.txt', false]; yield ['%BASE%/foo/bar.txt', false]; yield ['/', false]; @@ -205,14 +187,14 @@ class AttachmentTest extends TestCase /** * @dataProvider builtinDataProvider */ - public function testIsBuiltIn($path, $expected): void + public function testIsBuiltIn(?string $path, $expected): void { $attachment = new PartAttachment(); - $this->setProtectedProperty($attachment, 'path', $path); + $this->setProtectedProperty($attachment, 'internal_path', $path); $this->assertSame($expected, $attachment->isBuiltIn()); } - public function hostDataProvider(): \Iterator + public static function hostDataProvider(): \Iterator { yield ['%MEDIA%/foo/bar.txt', null]; yield ['https://www.google.de/test.txt', 'www.google.de']; @@ -222,55 +204,60 @@ class AttachmentTest extends TestCase /** * @dataProvider hostDataProvider */ - public function testGetHost($path, $expected): void + public function testGetHost(?string $path, ?string $expected): void { $attachment = new PartAttachment(); - $this->setProtectedProperty($attachment, 'path', $path); + $this->setProtectedProperty($attachment, 'external_path', $path); $this->assertSame($expected, $attachment->getHost()); } - public function filenameProvider(): \Iterator + public static function filenameProvider(): \Iterator { - yield ['%MEDIA%/foo/bar.txt', null, 'bar.txt']; - yield ['%MEDIA%/foo/bar.JPeg', 'test.txt', 'test.txt']; - yield ['https://www.google.de/test.txt', null, null]; + yield ['%MEDIA%/foo/bar.txt', 'https://www.google.de/test.txt', null, 'bar.txt']; + yield ['%MEDIA%/foo/bar.JPeg', 'https://www.google.de/foo.txt', 'test.txt', 'test.txt']; + yield ['', 'https://www.google.de/test.txt', null, null]; + yield [null, 'https://www.google.de/test.txt', null, null]; } /** * @dataProvider filenameProvider */ - public function testGetFilename($path, $original_filename, $expected): void + public function testGetFilename(?string $internal_path, ?string $external_path, ?string $original_filename, ?string $expected): void { $attachment = new PartAttachment(); - $this->setProtectedProperty($attachment, 'path', $path); + $this->setProtectedProperty($attachment, 'internal_path', $internal_path); + $this->setProtectedProperty($attachment, 'external_path', $external_path); $this->setProtectedProperty($attachment, 'original_filename', $original_filename); $this->assertSame($expected, $attachment->getFilename()); } - public function testSetURL(): void + public function testSetExternalPath(): void { $attachment = new PartAttachment(); //Set URL - $attachment->setURL('https://google.de'); - $this->assertSame('https://google.de', $attachment->getURL()); + $attachment->setExternalPath('https://google.de'); + $this->assertSame('https://google.de', $attachment->getExternalPath()); - //Ensure that an empty url does not overwrite the existing one - $attachment->setPath('%MEDIA%/foo/bar.txt'); - $attachment->setURL(' '); - $this->assertSame('%MEDIA%/foo/bar.txt', $attachment->getPath()); + //Ensure that changing the external path does reset the internal one + $attachment->setInternalPath('%MEDIA%/foo/bar.txt'); + $attachment->setExternalPath('https://example.de'); + $this->assertSame(null, $attachment->getInternalPath()); + + //Ensure that setting the same value to the external path again doesn't reset the internal one + $attachment->setExternalPath('https://google.de'); + $attachment->setInternalPath('%MEDIA%/foo/bar.txt'); + $attachment->setExternalPath('https://google.de'); + $this->assertSame('%MEDIA%/foo/bar.txt', $attachment->getInternalPath()); + + //Ensure that resetting the external path doesn't reset the internal one + $attachment->setInternalPath('%MEDIA%/foo/bar.txt'); + $attachment->setExternalPath(''); + $this->assertSame('%MEDIA%/foo/bar.txt', $attachment->getInternalPath()); //Ensure that spaces get replaced by %20 - $attachment->setURL('https://google.de/test file.txt'); - $this->assertSame('https://google.de/test%20file.txt', $attachment->getURL()); - } - - public function testSetURLForbiddenURL(): void - { - $attachment = new PartAttachment(); - - $this->expectException(InvalidArgumentException::class); - $attachment->setURL('%MEDIA%/foo/bar.txt'); + $attachment->setExternalPath('https://example.de/test file.txt'); + $this->assertSame('https://example.de/test%20file.txt', $attachment->getExternalPath()); } public function testIsURL(): void diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 77dd22da..41388de3 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -242,7 +242,7 @@ part.info.timetravel_hint - This is how the part appeared before %timestamp%. <i>Please note that this feature is experimental, so the info may not be correct.</i> + Please note that this feature is experimental, so the info may not be correct.]]> @@ -731,10 +731,10 @@ user.edit.tfa.disable_tfa_message - This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>! -<br> -The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br> -<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b> + all active two-factor authentication methods of the user and delete the backup codes! +
+The user will have to set up all two-factor authentication methods again and print new backup codes!

+Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!]]>
@@ -780,18 +780,10 @@ The user will have to set up all two-factor authentication methods again and pri Delete - - - Part-DB1\templates\AdminPages\_attachments.html.twig:41 - Part-DB1\templates\Parts\edit\_attachments.html.twig:38 - Part-DB1\templates\Parts\info\_attachments_info.html.twig:35 - Part-DB1\src\DataTables\AttachmentDataTable.php:159 - Part-DB1\templates\Parts\edit\_attachments.html.twig:38 - Part-DB1\src\DataTables\AttachmentDataTable.php:159 - + - attachment.external - External + attachment.external_only + External only @@ -806,7 +798,7 @@ The user will have to set up all two-factor authentication methods again and pri Attachment thumbnail - + Part-DB1\templates\AdminPages\_attachments.html.twig:52 Part-DB1\templates\Parts\edit\_attachments.html.twig:50 @@ -816,8 +808,8 @@ The user will have to set up all two-factor authentication methods again and pri Part-DB1\templates\Parts\info\_attachments_info.html.twig:45 - attachment.view - View + attachment.view_local + View Local Copy @@ -893,9 +885,9 @@ The user will have to set up all two-factor authentication methods again and pri entity.delete.message - This can not be undone! -<br> -Sub elements will be moved upwards. + +Sub elements will be moved upwards.]]> @@ -1449,7 +1441,7 @@ Sub elements will be moved upwards. homepage.github.text - Source, downloads, bug reports, to-do-list etc. can be found on <a href="%href%" class="link-external" target="_blank">GitHub project page</a> + GitHub project page]]> @@ -1471,7 +1463,7 @@ Sub elements will be moved upwards. homepage.help.text - Help and tips can be found in Wiki the <a href="%href%" class="link-external" target="_blank">GitHub page</a> + GitHub page]]> @@ -1713,7 +1705,7 @@ Sub elements will be moved upwards. email.pw_reset.fallback - If this does not work for you, go to <a href="%url%">%url%</a> and enter the following info + %url% and enter the following info]]> @@ -1743,7 +1735,7 @@ Sub elements will be moved upwards. email.pw_reset.valid_unit %date% - The reset token will be valid until <i>%date%</i>. + %date%.]]> @@ -2119,14 +2111,14 @@ Sub elements will be moved upwards. Preview picture - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:67 Part-DB1\templates\Parts\info\_attachments_info.html.twig:50 - attachment.download - Download + attachment.download_local + Download Local Copy @@ -3586,8 +3578,8 @@ Sub elements will be moved upwards. tfa_google.disable.confirm_message - If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.<br> -Also note that without two-factor authentication, your account is no longer as well protected against attackers! + +Also note that without two-factor authentication, your account is no longer as well protected against attackers!]]> @@ -3607,7 +3599,7 @@ Also note that without two-factor authentication, your account is no longer as w tfa_google.step.download - Download an authenticator app (e.g. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> oder <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>) + Google Authenticator oder FreeOTP Authenticator)]]> @@ -3849,8 +3841,8 @@ Also note that without two-factor authentication, your account is no longer as w tfa_trustedDevices.explanation - When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed. -If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of <i>all </i>computers here. + all computers here.]]> @@ -5321,7 +5313,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can label_options.lines_mode.help - If you select Twig here, the content field is interpreted as Twig template. See <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig documentation</a> and <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a> for more information. + Twig documentation and Wiki for more information.]]> @@ -9396,25 +9388,25 @@ Element 3 filter.parameter_value_constraint.operator.< - Typ. Value < + filter.parameter_value_constraint.operator.> - Typ. Value > + ]]> filter.parameter_value_constraint.operator.<= - Typ. Value <= + filter.parameter_value_constraint.operator.>= - Typ. Value >= + =]]> @@ -9522,7 +9514,7 @@ Element 3 parts_list.search.searching_for - Searching parts with keyword <b>%keyword%</b> + %keyword%]]> @@ -10182,13 +10174,13 @@ Element 3 project.builds.number_of_builds_possible - You have enough stocked to build <b>%max_builds%</b> builds of this project. + %max_builds% builds of this project.]]> project.builds.check_project_status - The current project status is <b>"%project_status%"</b>. You should check if you really want to build the project with this status! + "%project_status%". You should check if you really want to build the project with this status!]]> @@ -10290,7 +10282,7 @@ Element 3 entity.select.add_hint - Use -> to create nested structures, e.g. "Node 1->Node 1.1" + to create nested structures, e.g. "Node 1->Node 1.1"]]> @@ -10314,13 +10306,13 @@ Element 3 homepage.first_steps.introduction - Your database is still empty. You might want to read the <a href="%url%">documentation</a> or start to creating the following data structures: + documentation or start to creating the following data structures:]]> homepage.first_steps.create_part - Or you can directly <a href="%url%">create a new part</a>. + create a new part.]]> @@ -10332,7 +10324,7 @@ Element 3 homepage.forum.text - For questions about Part-DB use the <a href="%href%" class="link-external" target="_blank">discussion forum</a> + discussion forum]]> @@ -10986,7 +10978,7 @@ Element 3 parts.import.help_documentation - See the <a href="%link%">documentation</a> for more information on the file format. + documentation for more information on the file format.]]> @@ -11166,7 +11158,7 @@ Element 3 part.filter.lessThanDesired - In stock less than desired (total amount < min. amount) + @@ -11978,13 +11970,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g part.merge.confirm.title - Do you really want to merge <b>%other%</b> into <b>%target%</b>? + %other% into %target%?]]> part.merge.confirm.message - <b>%other%</b> will be deleted, and the part will be saved with the shown information. + %other% will be deleted, and the part will be saved with the shown information.]]> @@ -12329,5 +12321,29 @@ Please note, that you can not impersonate a disabled user. If you try you will g There are no entities to export! + + + attachment.table.internal_file + Internal file + + + + + attachment.table.external_link + External link + + + + + attachment.view_external.view_at + View at %host% + + + + + attachment.view_external + View external version + + From 48be9a8098c94ffa517b899ae574608d06d4f321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 17:41:41 +0100 Subject: [PATCH 02/18] Made attachment datatable sortable by internal filename and external url --- migrations/Version20250220215048.php | 4 ---- src/DataTables/AttachmentDataTable.php | 7 +++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/migrations/Version20250220215048.php b/migrations/Version20250220215048.php index 82e132de..fd4fb9c2 100644 --- a/migrations/Version20250220215048.php +++ b/migrations/Version20250220215048.php @@ -6,11 +6,7 @@ namespace DoctrineMigrations; use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20250220215048 extends AbstractMultiPlatformMigration { public function getDescription(): string diff --git a/src/DataTables/AttachmentDataTable.php b/src/DataTables/AttachmentDataTable.php index 7973bf8a..16e6a7a7 100644 --- a/src/DataTables/AttachmentDataTable.php +++ b/src/DataTables/AttachmentDataTable.php @@ -119,6 +119,7 @@ final class AttachmentDataTable implements DataTableTypeInterface $dataTable->add('internal_link', TextColumn::class, [ 'label' => 'attachment.table.internal_file', 'propertyPath' => 'filename', + 'orderField' => 'NATSORT(attachment.original_filename)', 'render' => function ($value, Attachment $context) { if ($this->attachmentHelper->isInternalFileExisting($context)) { return sprintf( @@ -135,12 +136,14 @@ final class AttachmentDataTable implements DataTableTypeInterface $dataTable->add('external_link', TextColumn::class, [ 'label' => 'attachment.table.external_link', 'propertyPath' => 'host', + 'orderField' => 'attachment.external_path', 'render' => function ($value, Attachment $context) { if ($context->hasExternal()) { return sprintf( - '%s', + '%s', htmlspecialchars((string) $context->getExternalPath()), - htmlspecialchars($value) + htmlspecialchars((string) $context->getExternalPath()), + htmlspecialchars($value), ); } From f146d88aa57ce42934bfc2c0dcf762beb3de5340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 17:48:26 +0100 Subject: [PATCH 03/18] Added additional filters to attachment datatable --- src/DataTables/Filters/AttachmentFilter.php | 6 ++++++ src/Form/Filters/AttachmentFilterType.php | 9 +++++++++ templates/attachment_list.html.twig | 2 ++ 3 files changed, 17 insertions(+) diff --git a/src/DataTables/Filters/AttachmentFilter.php b/src/DataTables/Filters/AttachmentFilter.php index 9f8cf094..d41bbe39 100644 --- a/src/DataTables/Filters/AttachmentFilter.php +++ b/src/DataTables/Filters/AttachmentFilter.php @@ -45,6 +45,9 @@ class AttachmentFilter implements FilterInterface public readonly DateTimeConstraint $lastModified; public readonly DateTimeConstraint $addedDate; + public readonly TextConstraint $originalFileName; + public readonly TextConstraint $externalLink; + public function __construct(NodesListBuilder $nodesListBuilder) { @@ -55,6 +58,9 @@ class AttachmentFilter implements FilterInterface $this->lastModified = new DateTimeConstraint('attachment.lastModified'); $this->addedDate = new DateTimeConstraint('attachment.addedDate'); $this->showInTable = new BooleanConstraint('attachment.show_in_table'); + $this->originalFileName = new TextConstraint('attachment.original_filename'); + $this->externalLink = new TextConstraint('attachment.external_path'); + } public function apply(QueryBuilder $queryBuilder): void diff --git a/src/Form/Filters/AttachmentFilterType.php b/src/Form/Filters/AttachmentFilterType.php index e6746feb..ff80bd38 100644 --- a/src/Form/Filters/AttachmentFilterType.php +++ b/src/Form/Filters/AttachmentFilterType.php @@ -100,6 +100,15 @@ class AttachmentFilterType extends AbstractType 'label' => 'attachment.edit.show_in_table' ]); + $builder->add('originalFileName', TextConstraintType::class, [ + 'label' => 'attachment.file_name' + ]); + + $builder->add('externalLink', TextConstraintType::class, [ + 'label' => 'attachment.table.external_link' + ]); + + $builder->add('lastModified', DateTimeConstraintType::class, [ 'label' => 'lastModified' ]); diff --git a/templates/attachment_list.html.twig b/templates/attachment_list.html.twig index abb6f4ad..3ff45700 100644 --- a/templates/attachment_list.html.twig +++ b/templates/attachment_list.html.twig @@ -34,6 +34,8 @@ {{ form_row(filterForm.attachmentType) }} {{ form_row(filterForm.targetType) }} {{ form_row(filterForm.showInTable) }} + {{ form_row(filterForm.originalFileName) }} + {{ form_row(filterForm.externalLink) }} {{ form_row(filterForm.lastModified) }} {{ form_row(filterForm.addedDate) }} {{ form_row(filterForm.dbId) }} From 019e67a67630f306c404f42dd7fcae05d36e8a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 17:58:20 +0100 Subject: [PATCH 04/18] Migrate legacy attachment discriminator class values to modern format, so that we can make the discriminator map unique and fix a deprecation with doctrine --- migrations/Version20250222165240.php | 31 +++++++++++++++++++++++++++ src/Entity/Attachments/Attachment.php | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 migrations/Version20250222165240.php diff --git a/migrations/Version20250222165240.php b/migrations/Version20250222165240.php new file mode 100644 index 00000000..5dd73d44 --- /dev/null +++ b/migrations/Version20250222165240.php @@ -0,0 +1,31 @@ +addSql('UPDATE attachments SET class_name = "Part" WHERE class_name = "PartDB\Part"'); + $this->addSql('UPDATE attachments SET class_name = "Device" WHERE class_name = "PartDB\Device"'); + } + + public function down(Schema $schema): void + { + //No down required, as the new format can also be read by older Part-DB version + } +} diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 3c8d890d..4847eea5 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -96,8 +96,8 @@ use LogicException; #[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] abstract class Attachment extends AbstractNamedDBElement { - private const ORM_DISCRIMINATOR_MAP = ['PartDB\Part' => PartAttachment::class, 'Part' => PartAttachment::class, - 'PartDB\Device' => ProjectAttachment::class, 'Device' => ProjectAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class, + private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, + 'AttachmentType' => AttachmentTypeAttachment::class, 'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class, 'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class, 'Storelocation' => StorageLocationAttachment::class, 'Supplier' => SupplierAttachment::class, From bec45d60e5a766f22319dde63453d18c6c23605e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 18:03:03 +0100 Subject: [PATCH 05/18] Fixed migration for postgresql --- migrations/Version20250222165240.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/Version20250222165240.php b/migrations/Version20250222165240.php index 5dd73d44..84e4f709 100644 --- a/migrations/Version20250222165240.php +++ b/migrations/Version20250222165240.php @@ -20,8 +20,8 @@ final class Version20250222165240 extends AbstractMigration public function up(Schema $schema): void { //Change the old discriminator values to the new ones - $this->addSql('UPDATE attachments SET class_name = "Part" WHERE class_name = "PartDB\Part"'); - $this->addSql('UPDATE attachments SET class_name = "Device" WHERE class_name = "PartDB\Device"'); + $this->addSql("UPDATE attachments SET class_name = 'Part' WHERE class_name = 'PartDB\Part'"); + $this->addSql("UPDATE attachments SET class_name = 'Device' WHERE class_name = ''PartDB\Device'"); } public function down(Schema $schema): void From 6fd05e145688aedaebc65aec90050b9099c2b804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 19:23:28 +0100 Subject: [PATCH 06/18] Fixed migration --- migrations/Version20250222165240.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/Version20250222165240.php b/migrations/Version20250222165240.php index 84e4f709..57cd3970 100644 --- a/migrations/Version20250222165240.php +++ b/migrations/Version20250222165240.php @@ -21,7 +21,7 @@ final class Version20250222165240 extends AbstractMigration { //Change the old discriminator values to the new ones $this->addSql("UPDATE attachments SET class_name = 'Part' WHERE class_name = 'PartDB\Part'"); - $this->addSql("UPDATE attachments SET class_name = 'Device' WHERE class_name = ''PartDB\Device'"); + $this->addSql("UPDATE attachments SET class_name = 'Device' WHERE class_name = 'PartDB\Device'"); } public function down(Schema $schema): void From 42cb590c75cd2d72cbb54af7106eaf877979140e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 19:35:49 +0100 Subject: [PATCH 07/18] Fixed deprecations with api platform --- config/packages/api_platform.yaml | 6 +++++- src/ApiPlatform/Filter/EntityFilterHelper.php | 6 ------ src/ApiPlatform/Filter/LikeFilter.php | 6 ------ src/ApiPlatform/Filter/TagFilter.php | 6 ------ 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index b32ddac7..d55f91ea 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -34,4 +34,8 @@ api_platform: keep_legacy_inflector: false # Need to be true, or some tests will fail - use_symfony_listeners: true \ No newline at end of file + use_symfony_listeners: true + + serializer: + # Change this to false later, to remove the hydra prefix on the API + hydra_prefix: true \ No newline at end of file diff --git a/src/ApiPlatform/Filter/EntityFilterHelper.php b/src/ApiPlatform/Filter/EntityFilterHelper.php index 42cc567f..45e04fde 100644 --- a/src/ApiPlatform/Filter/EntityFilterHelper.php +++ b/src/ApiPlatform/Filter/EntityFilterHelper.php @@ -92,12 +92,6 @@ class EntityFilterHelper 'type' => Type::BUILTIN_TYPE_STRING, 'required' => false, 'description' => 'Filter using a comma seperated list of element IDs. Use + to include all direct children and ++ to include all children recursively.', - 'openapi' => [ - 'example' => '', - 'allowReserved' => false,// if true, query parameters will be not percent-encoded - 'allowEmptyValue' => true, - 'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green - ], ]; } return $description; diff --git a/src/ApiPlatform/Filter/LikeFilter.php b/src/ApiPlatform/Filter/LikeFilter.php index a70cfd9e..a8e96eb9 100644 --- a/src/ApiPlatform/Filter/LikeFilter.php +++ b/src/ApiPlatform/Filter/LikeFilter.php @@ -67,12 +67,6 @@ final class LikeFilter extends AbstractFilter 'type' => Type::BUILTIN_TYPE_STRING, 'required' => false, 'description' => 'Filter using a LIKE SQL expression. Use % as wildcard for multiple characters and _ for single characters. For example, to search for all items containing foo, use foo. To search for all items starting with foo, use foo%. To search for all items ending with foo, use %foo', - 'openapi' => [ - 'example' => '', - 'allowReserved' => false,// if true, query parameters will be not percent-encoded - 'allowEmptyValue' => true, - 'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green - ], ]; } return $description; diff --git a/src/ApiPlatform/Filter/TagFilter.php b/src/ApiPlatform/Filter/TagFilter.php index b8be0657..98648ee9 100644 --- a/src/ApiPlatform/Filter/TagFilter.php +++ b/src/ApiPlatform/Filter/TagFilter.php @@ -89,12 +89,6 @@ final class TagFilter extends AbstractFilter 'type' => Type::BUILTIN_TYPE_STRING, 'required' => false, 'description' => 'Filter for tags of a part', - 'openapi' => [ - 'example' => '', - 'allowReserved' => false,// if true, query parameters will be not percent-encoded - 'allowEmptyValue' => true, - 'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green - ], ]; } return $description; From 17caf476bf5c2c6b657f5d9ecd53896667b4b8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 19:48:36 +0100 Subject: [PATCH 08/18] Use the modular api-platform packages instead of the monolitic api-platform/core package --- composer.json | 4 +- composer.lock | 1368 +++++++++++++++++++++++++++++++----- src/ApiResource/.gitignore | 0 symfony.lock | 14 + 4 files changed, 1220 insertions(+), 166 deletions(-) create mode 100644 src/ApiResource/.gitignore diff --git a/composer.json b/composer.json index 3b2904d6..94a91a0a 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,9 @@ "ext-json": "*", "ext-mbstring": "*", "amphp/http-client": "^5.1", - "api-platform/core": "^3.1", + "api-platform/doctrine-orm": "^3", + "api-platform/json-api": "^3.4", + "api-platform/symfony": "^3", "beberlei/doctrineextensions": "^1.2", "brick/math": "0.12.1 as 0.11.0", "composer/ca-bundle": "^1.3", diff --git a/composer.lock b/composer.lock index e2b6eb60..e4d3b662 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "75643d42e05fce4684644d375bff2d0a", + "content-hash": "e8f14b530c4184d1e512e7d1687c71e5", "packages": [ { "name": "amphp/amp", @@ -967,179 +967,48 @@ "time": "2024-08-03T19:31:26+00:00" }, { - "name": "api-platform/core", + "name": "api-platform/doctrine-common", "version": "v3.4.16", "source": { "type": "git", - "url": "https://github.com/api-platform/core.git", - "reference": "64c6e1092cf988ba619907b3e4cce8a229ce4fae" + "url": "https://github.com/api-platform/doctrine-common.git", + "reference": "07c8dbca43861691d86c84b627870acf61976a74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/64c6e1092cf988ba619907b3e4cce8a229ce4fae", - "reference": "64c6e1092cf988ba619907b3e4cce8a229ce4fae", + "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/07c8dbca43861691d86c84b627870acf61976a74", + "reference": "07c8dbca43861691d86c84b627870acf61976a74", "shasum": "" }, "require": { - "doctrine/inflector": "^1.0 || ^2.0", - "php": ">=8.1", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^3.1", - "symfony/http-foundation": "^6.4 || ^7.1", - "symfony/http-kernel": "^6.4 || ^7.1", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", - "symfony/translation-contracts": "^3.3", - "symfony/web-link": "^6.4 || ^7.1", - "willdurand/negotiation": "^3.0" + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "doctrine/collections": "^2.1", + "doctrine/common": "^3.2.2", + "doctrine/persistence": "^3.2", + "php": ">=8.1" }, "conflict": { - "doctrine/common": "<3.2.2", - "doctrine/dbal": "<2.10", - "doctrine/mongodb-odm": "<2.4", - "doctrine/orm": "<2.14.0", - "doctrine/persistence": "<1.3", - "elasticsearch/elasticsearch": ">=8.0,<8.4", - "phpspec/prophecy": "<1.15", - "phpunit/phpunit": "<9.5", - "symfony/framework-bundle": "6.4.6 || 7.0.6", - "symfony/var-exporter": "<6.1.1" - }, - "replace": { - "api-platform/doctrine-common": "self.version", - "api-platform/doctrine-odm": "self.version", - "api-platform/doctrine-orm": "self.version", - "api-platform/documentation": "self.version", - "api-platform/elasticsearch": "self.version", - "api-platform/graphql": "self.version", - "api-platform/http-cache": "self.version", - "api-platform/hydra": "self.version", - "api-platform/json-api": "self.version", - "api-platform/json-hal": "self.version", - "api-platform/json-schema": "self.version", - "api-platform/jsonld": "self.version", - "api-platform/laravel": "self.version", - "api-platform/metadata": "self.version", - "api-platform/openapi": "self.version", - "api-platform/parameter-validator": "self.version", - "api-platform/ramsey-uuid": "self.version", - "api-platform/serializer": "self.version", - "api-platform/state": "self.version", - "api-platform/symfony": "self.version", - "api-platform/validator": "self.version" + "doctrine/persistence": "<1.3" }, "require-dev": { - "api-platform/doctrine-common": "^3.4 || ^4.0", - "api-platform/doctrine-odm": "^3.4 || ^4.0", - "api-platform/doctrine-orm": "^3.4 || ^4.0", - "api-platform/documentation": "^3.4 || ^4.0", - "api-platform/elasticsearch": "^3.4 || ^4.0", - "api-platform/graphql": "^3.4 || ^4.0", - "api-platform/http-cache": "^3.4 || ^4.0", - "api-platform/hydra": "^3.4 || ^4.0", - "api-platform/json-api": "^3.3 || ^4.0", - "api-platform/json-schema": "^3.4 || ^4.0", - "api-platform/jsonld": "^3.4 || ^4.0", - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/openapi": "^3.4 || ^4.0", - "api-platform/parameter-validator": "^3.4", - "api-platform/ramsey-uuid": "^3.4 || ^4.0", - "api-platform/serializer": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "api-platform/validator": "^3.4 || ^4.0", - "behat/behat": "^3.11", - "behat/mink": "^1.9", - "doctrine/cache": "^1.11 || ^2.1", - "doctrine/common": "^3.2.2", - "doctrine/dbal": "^3.4.0 || ^4.0", - "doctrine/doctrine-bundle": "^1.12 || ^2.0", - "doctrine/mongodb-odm": "^2.2", - "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", - "doctrine/orm": "^2.14 || ^3.0", - "elasticsearch/elasticsearch": "^7.11 || ^8.4", - "friends-of-behat/mink-browserkit-driver": "^1.3.1", - "friends-of-behat/mink-extension": "^2.2", - "friends-of-behat/symfony-extension": "^2.1", - "guzzlehttp/guzzle": "^6.0 || ^7.1", - "jangregor/phpstan-prophecy": "^1.0", - "justinrainbow/json-schema": "^5.2.1", + "doctrine/mongodb-odm": "^2.6", + "doctrine/orm": "^2.17 || ^3.0", "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpdoc-parser": "^1.13|^2.0", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-doctrine": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-symfony": "^1.0", - "phpunit/phpunit": "^9.6", - "psr/log": "^1.0 || ^2.0 || ^3.0", - "ramsey/uuid": "^3.9.7 || ^4.0", - "ramsey/uuid-doctrine": "^1.4 || ^2.0 || ^3.0", - "sebastian/comparator": "<5.0", - "soyuka/contexts": "v3.3.9", - "soyuka/pmu": "^0.0.12", - "soyuka/stubs-mongodb": "^1.0", - "symfony/asset": "^6.4 || ^7.1", - "symfony/browser-kit": "^6.4 || ^7.1", - "symfony/cache": "^6.4 || ^7.1", - "symfony/config": "^6.4 || ^7.1", - "symfony/console": "^6.4 || ^7.1", - "symfony/css-selector": "^6.4 || ^7.1", - "symfony/dependency-injection": "^6.4 || ^7.1", - "symfony/doctrine-bridge": "^6.4 || ^7.1", - "symfony/dom-crawler": "^6.4 || ^7.1", - "symfony/error-handler": "^6.4 || ^7.1", - "symfony/event-dispatcher": "^6.4 || ^7.1", - "symfony/expression-language": "^6.4 || ^7.1", - "symfony/finder": "^6.4 || ^7.1", - "symfony/form": "^6.4 || ^7.1", - "symfony/framework-bundle": "^6.4 || ^7.1", - "symfony/http-client": "^6.4 || ^7.1", - "symfony/intl": "^6.4 || ^7.1", - "symfony/maker-bundle": "^1.24", - "symfony/mercure-bundle": "*", - "symfony/messenger": "^6.4 || ^7.1", - "symfony/phpunit-bridge": "^6.4.1 || ^7.1", - "symfony/routing": "^6.4 || ^7.1", - "symfony/security-bundle": "^6.4 || ^7.1", - "symfony/security-core": "^6.4 || ^7.1", - "symfony/stopwatch": "^6.4 || ^7.1", - "symfony/string": "^6.4 || ^7.1", - "symfony/twig-bundle": "^6.4 || ^7.1", - "symfony/uid": "^6.4 || ^7.1", - "symfony/validator": "^6.4 || ^7.1", - "symfony/web-profiler-bundle": "^6.4 || ^7.1", - "symfony/yaml": "^6.4 || ^7.1", - "twig/twig": "^1.42.3 || ^2.12 || ^3.0", - "webonyx/graphql-php": "^14.0 || ^15.0" + "phpunit/phpunit": "^10.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0" }, "suggest": { - "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", - "elasticsearch/elasticsearch": "To support Elasticsearch.", - "ocramius/package-versions": "To display the API Platform's version in the debug bar.", - "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", - "psr/cache-implementation": "To use metadata caching.", - "ramsey/uuid": "To support Ramsey's UUID identifiers.", - "symfony/cache": "To have metadata caching when using Symfony integration.", - "symfony/config": "To load XML configuration files.", - "symfony/expression-language": "To use authorization features.", - "symfony/http-client": "To use the HTTP cache invalidation system.", - "symfony/messenger": "To support messenger integration.", - "symfony/security": "To use authorization features.", - "symfony/twig-bundle": "To use the Swagger UI integration.", - "symfony/uid": "To support Symfony UUID/ULID identifiers.", - "symfony/web-profiler-bundle": "To use the data collector.", - "webonyx/graphql-php": "To support GraphQL." + "api-platform/graphql": "For GraphQl mercure subscriptions.", + "api-platform/http-cache": "For HTTP cache invalidation.", + "phpstan/phpdoc-parser": "For PHP documentation support.", + "symfony/config": "For XML resource configuration.", + "symfony/mercure-bundle": "For mercure updates publisher.", + "symfony/messenger": "For async mercure updates.", + "symfony/yaml": "For YAML resource configuration." }, "type": "library", "extra": { - "pmu": { - "projects": [ - "./src/*/composer.json", - "src/Doctrine/*/composer.json" - ] - }, "thanks": { "url": "https://github.com/api-platform/api-platform", "name": "api-platform/api-platform" @@ -1154,7 +1023,7 @@ }, "autoload": { "psr-4": { - "ApiPlatform\\": "src/" + "ApiPlatform\\Doctrine\\Common\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1166,9 +1035,634 @@ "name": "Kévin Dunglas", "email": "kevin@dunglas.fr", "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" } ], - "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", + "description": "Common files used by api-platform/doctrine-orm and api-platform/doctrine-odm", + "homepage": "https://api-platform.com", + "keywords": [ + "doctrine", + "graphql", + "odm", + "orm", + "rest" + ], + "support": { + "source": "https://github.com/api-platform/doctrine-common/tree/v3.4.16" + }, + "time": "2024-09-21T10:54:56+00:00" + }, + { + "name": "api-platform/doctrine-orm", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/doctrine-orm.git", + "reference": "01d49d6f89060eaf4804b02bc0f8d04f59107999" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/01d49d6f89060eaf4804b02bc0f8d04f59107999", + "reference": "01d49d6f89060eaf4804b02bc0f8d04f59107999", + "shasum": "" + }, + "require": { + "api-platform/doctrine-common": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "doctrine/orm": "^2.17 || ^3.0", + "php": ">=8.1", + "symfony/property-info": "^6.4 || ^7.1" + }, + "require-dev": { + "api-platform/parameter-validator": "^3.2", + "doctrine/doctrine-bundle": "^2.11", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^10.0", + "ramsey/uuid": "^4.7", + "ramsey/uuid-doctrine": "^2.0", + "symfony/cache": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", + "symfony/uid": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Doctrine\\Orm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "Doctrine ORM bridge", + "homepage": "https://api-platform.com", + "keywords": [ + "api", + "doctrine", + "graphql", + "orm", + "rest" + ], + "support": { + "source": "https://github.com/api-platform/doctrine-orm/tree/v3.4.16" + }, + "time": "2024-12-09T10:30:13+00:00" + }, + { + "name": "api-platform/documentation", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/documentation.git", + "reference": "056f61476ec408aef16c9aca514e2b9514fa8f1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/documentation/zipball/056f61476ec408aef16c9aca514e2b9514fa8f1e", + "reference": "056f61476ec408aef16c9aca514e2b9514fa8f1e", + "shasum": "" + }, + "require": { + "api-platform/metadata": "^3.4 || ^4.0" + }, + "type": "project", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Documentation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "API Platform documentation controller.", + "support": { + "source": "https://github.com/api-platform/documentation/tree/v3.4.16" + }, + "time": "2024-09-21T10:54:56+00:00" + }, + { + "name": "api-platform/http-cache", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/http-cache.git", + "reference": "66abd8e964f4d5ce1f91802d0ec2b1938abd0fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/http-cache/zipball/66abd8e964f4d5ce1f91802d0ec2b1938abd0fab", + "reference": "66abd8e964f4d5ce1f91802d0ec2b1938abd0fab", + "shasum": "" + }, + "require": { + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "php": ">=8.1", + "symfony/http-foundation": "^6.4 || ^7.1" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "phpspec/prophecy-phpunit": "^2.0", + "sebastian/comparator": "<5.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/http-client": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\HttpCache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/comunnity/contributors" + } + ], + "description": "API Platform HttpCache component", + "homepage": "https://api-platform.com", + "keywords": [ + "api", + "cache", + "http", + "rest" + ], + "support": { + "source": "https://github.com/api-platform/http-cache/tree/v3.4.16" + }, + "time": "2024-10-26T07:09:46+00:00" + }, + { + "name": "api-platform/hydra", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/hydra.git", + "reference": "db4a2c416ebd749864bd116c87b9db566adb3894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/hydra/zipball/db4a2c416ebd749864bd116c87b9db566adb3894", + "reference": "db4a2c416ebd749864bd116c87b9db566adb3894", + "shasum": "" + }, + "require": { + "api-platform/documentation": "^3.4 || ^4.0", + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/jsonld": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/serializer": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "php": ">=8.1" + }, + "require-dev": { + "api-platform/doctrine-common": "^3.4 || ^4.0", + "api-platform/doctrine-odm": "^3.4 || ^4.0", + "api-platform/doctrine-orm": "^3.4 || ^4.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Hydra\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "API Hydra support", + "homepage": "https://api-platform.com", + "keywords": [ + "Hydra", + "JSON-LD", + "api", + "graphql", + "jsonapi", + "rest" + ], + "support": { + "source": "https://github.com/api-platform/hydra/tree/v3.4.16" + }, + "time": "2024-10-25T09:44:09+00:00" + }, + { + "name": "api-platform/json-api", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/json-api.git", + "reference": "6f86528eddc9150e8e0c7a7dbacbca679de81cb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/json-api/zipball/6f86528eddc9150e8e0c7a7dbacbca679de81cb6", + "reference": "6f86528eddc9150e8e0c7a7dbacbca679de81cb6", + "shasum": "" + }, + "require": { + "api-platform/documentation": "^3.4 || ^4.0", + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/serializer": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "php": ">=8.1", + "symfony/error-handler": "^6.4 || ^7.1", + "symfony/http-foundation": "^6.4 || ^7.1" + }, + "require-dev": { + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^11.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\JsonApi\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "API JSON-API support", + "homepage": "https://api-platform.com", + "keywords": [ + "api", + "jsonapi", + "rest" + ], + "support": { + "source": "https://github.com/api-platform/json-api/tree/v3.4.16" + }, + "time": "2024-12-13T10:57:36+00:00" + }, + { + "name": "api-platform/json-schema", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/json-schema.git", + "reference": "a1dff7eebefd3a64799418c8a2cc40fff4ab42fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/json-schema/zipball/a1dff7eebefd3a64799418c8a2cc40fff4ab42fc", + "reference": "a1dff7eebefd3a64799418c8a2cc40fff4ab42fc", + "shasum": "" + }, + "require": { + "api-platform/metadata": "^3.4 || ^4.0", + "php": ">=8.1", + "symfony/console": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", + "symfony/uid": "^6.4 || ^7.0" + }, + "require-dev": { + "phpspec/prophecy-phpunit": "^2.0", + "sebastian/comparator": "<5.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\JsonSchema\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "Generate a JSON Schema from a PHP class", + "homepage": "https://api-platform.com", + "keywords": [ + "JSON Schema", + "api", + "json", + "openapi", + "rest", + "swagger" + ], + "support": { + "source": "https://github.com/api-platform/json-schema/tree/v3.4.16" + }, + "time": "2025-01-16T07:34:23+00:00" + }, + { + "name": "api-platform/jsonld", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/jsonld.git", + "reference": "aab1d012fb19e00ced5f5deac5928b5fa5aadc23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/jsonld/zipball/aab1d012fb19e00ced5f5deac5928b5fa5aadc23", + "reference": "aab1d012fb19e00ced5f5deac5928b5fa5aadc23", + "shasum": "" + }, + "require": { + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/serializer": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\JsonLd\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "API JSON-LD support", + "homepage": "https://api-platform.com", + "keywords": [ + "Hydra", + "JSON-LD", + "api", + "graphql", + "rest" + ], + "support": { + "source": "https://github.com/api-platform/jsonld/tree/v3.4.16" + }, + "time": "2024-11-29T14:41:22+00:00" + }, + { + "name": "api-platform/metadata", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/metadata.git", + "reference": "931ccaf7a8df428ad5b18ae8a78b3266f41fec6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/931ccaf7a8df428ad5b18ae8a78b3266f41fec6f", + "reference": "931ccaf7a8df428ad5b18ae8a78b3266f41fec6f", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.0 || ^2.0", + "php": ">=8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/string": "^6.4 || ^7.0" + }, + "require-dev": { + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/openapi": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/phpdoc-parser": "^1.16", + "sebastian/comparator": "<5.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0", + "symfony/routing": "^6.4 || ^7.0", + "symfony/var-dumper": "^6.4 || ^7.0", + "symfony/web-link": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0" + }, + "suggest": { + "phpstan/phpdoc-parser": "For PHP documentation support.", + "symfony/config": "For XML resource configuration.", + "symfony/yaml": "For YAML resource configuration." + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Metadata\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "API Resource-oriented metadata attributes and factories", "homepage": "https://api-platform.com", "keywords": [ "Hydra", @@ -1182,10 +1676,462 @@ "swagger" ], "support": { - "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/v3.4.16" + "source": "https://github.com/api-platform/metadata/tree/v3.4.16" }, - "time": "2025-01-17T14:17:26+00:00" + "time": "2025-01-10T10:33:02+00:00" + }, + { + "name": "api-platform/openapi", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/openapi.git", + "reference": "3313d511f5257a1f0ed1929e7d809d02d63d90cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/3313d511f5257a1f0ed1929e7d809d02d63d90cf", + "reference": "3313d511f5257a1f0ed1929e7d809d02d63d90cf", + "shasum": "" + }, + "require": { + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "php": ">=8.1", + "symfony/console": "^6.4 || ^7.0", + "symfony/property-access": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1" + }, + "require-dev": { + "api-platform/doctrine-common": "^3.4 || ^4.0", + "api-platform/doctrine-odm": "^3.4 || ^4.0", + "api-platform/doctrine-orm": "^3.4 || ^4.0", + "phpspec/prophecy-phpunit": "^2.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\OpenApi\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "Models to build and serialize an OpenAPI specification.", + "homepage": "https://api-platform.com", + "keywords": [ + "Hydra", + "JSON-LD", + "api", + "graphql", + "hal", + "jsonapi", + "openapi", + "rest", + "swagger" + ], + "support": { + "source": "https://github.com/api-platform/openapi/tree/v3.4.16" + }, + "time": "2025-01-09T10:29:41+00:00" + }, + { + "name": "api-platform/serializer", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/serializer.git", + "reference": "acc3d2b59ac79e6f3284b01c03c7573efc61ddf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/serializer/zipball/acc3d2b59ac79e6f3284b01c03c7573efc61ddf1", + "reference": "acc3d2b59ac79e6f3284b01c03c7573efc61ddf1", + "shasum": "" + }, + "require": { + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "php": ">=8.1", + "symfony/property-access": "^6.4 || ^7.1", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", + "symfony/validator": "^6.4 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^2.1", + "phpspec/prophecy-phpunit": "^2.0", + "sebastian/comparator": "<5.0", + "symfony/mercure-bundle": "*", + "symfony/phpunit-bridge": "^6.4 || ^7.0", + "symfony/var-dumper": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0" + }, + "suggest": { + "api-platform/doctrine-odm": "To support Doctrine MongoDB ODM state options.", + "api-platform/doctrine-orm": "To support Doctrine ORM state options." + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "API Platform core Serializer", + "homepage": "https://api-platform.com", + "keywords": [ + "api", + "graphql", + "rest", + "serializer" + ], + "support": { + "source": "https://github.com/api-platform/serializer/tree/v3.4.16" + }, + "time": "2024-11-27T16:23:42+00:00" + }, + { + "name": "api-platform/state", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/state.git", + "reference": "b4a3e6f5e6e4bf1c9301b05cc2a08ddf5a6de94a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/state/zipball/b4a3e6f5e6e4bf1c9301b05cc2a08ddf5a6de94a", + "reference": "b4a3e6f5e6e4bf1c9301b05cc2a08ddf5a6de94a", + "shasum": "" + }, + "require": { + "api-platform/metadata": "^3.4 || ^4.0", + "php": ">=8.1", + "psr/container": "^1.0 || ^2.0", + "symfony/http-kernel": "^6.4 || ^7.0" + }, + "require-dev": { + "api-platform/serializer": "^3.4 || ^4.0", + "api-platform/validator": "^3.4 || ^4.0", + "phpunit/phpunit": "^10.3", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/web-link": "^6.4 || ^7.0", + "willdurand/negotiation": "^3.1" + }, + "suggest": { + "api-platform/serializer": "To use API Platform serializer.", + "api-platform/validator": "To use API Platform validation.", + "symfony/http-foundation": "To use our HTTP providers and processor.", + "symfony/web-link": "To support adding web links to the response headers.", + "willdurand/negotiation": "To use the API Platform content negoatiation provider." + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\State\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "API Platform State component ", + "homepage": "https://api-platform.com", + "keywords": [ + "Hydra", + "JSON-LD", + "api", + "graphql", + "hal", + "jsonapi", + "openapi", + "rest", + "swagger" + ], + "support": { + "source": "https://github.com/api-platform/state/tree/v3.4.16" + }, + "time": "2024-12-06T10:05:08+00:00" + }, + { + "name": "api-platform/symfony", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/symfony.git", + "reference": "b44c6e609850fafac719855fc9f83392dbd27825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/b44c6e609850fafac719855fc9f83392dbd27825", + "reference": "b44c6e609850fafac719855fc9f83392dbd27825", + "shasum": "" + }, + "require": { + "api-platform/documentation": "^3.4 || ^4.0", + "api-platform/http-cache": "^3.4 || ^4.0", + "api-platform/hydra": "^3.4 || ^4.0", + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/jsonld": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/openapi": "^3.4 || ^4.0", + "api-platform/serializer": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "api-platform/validator": "^3.4 || ^4.0", + "php": ">=8.1", + "symfony/property-access": "^6.4 || ^7.1", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/security-core": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.1", + "willdurand/negotiation": "^3.0" + }, + "require-dev": { + "api-platform/doctrine-common": "^3.4 || ^4.0", + "api-platform/doctrine-odm": "^3.4 || ^4.0", + "api-platform/doctrine-orm": "^3.4 || ^4.0", + "api-platform/elasticsearch": "^3.4 || ^4.0", + "api-platform/graphql": "^3.4 || ^4.0", + "api-platform/parameter-validator": "^3.1", + "phpspec/prophecy-phpunit": "^2.0", + "sebastian/comparator": "<5.0", + "symfony/expression-language": "^6.4 || ^7.1", + "symfony/mercure-bundle": "*", + "symfony/phpunit-bridge": "^6.4 || ^7.0", + "symfony/routing": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "webonyx/graphql-php": "^14.0 || ^15.0" + }, + "suggest": { + "api-platform/doctrine-odm": "To support MongoDB. Only versions 4.0 and later are supported.", + "api-platform/doctrine-orm": "To support Doctrine ORM.", + "api-platform/elasticsearch": "To support Elasticsearch.", + "api-platform/graphql": "To support GraphQL.", + "api-platform/ramsey-uuid": "To support Ramsey's UUID identifiers.", + "ocramius/package-versions": "To display the API Platform's version in the debug bar.", + "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", + "psr/cache-implementation": "To use metadata caching.", + "symfony/cache": "To have metadata caching when using Symfony integration.", + "symfony/config": "To load XML configuration files.", + "symfony/expression-language": "To use authorization and mercure advanced features.", + "symfony/http-client": "To use the HTTP cache invalidation system.", + "symfony/mercure-bundle": "To support mercure integration.", + "symfony/messenger": "To support messenger integration and asynchronous Mercure updates.", + "symfony/security": "To use authorization features.", + "symfony/twig-bundle": "To use the Swagger UI integration.", + "symfony/uid": "To support Symfony UUID/ULID identifiers.", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Symfony\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "Symfony API Platform integration", + "homepage": "https://api-platform.com", + "keywords": [ + "api", + "graphql", + "rest", + "symfony" + ], + "support": { + "source": "https://github.com/api-platform/symfony/tree/v3.4.16" + }, + "time": "2025-01-17T14:14:18+00:00" + }, + { + "name": "api-platform/validator", + "version": "v3.4.16", + "source": { + "type": "git", + "url": "https://github.com/api-platform/validator.git", + "reference": "d6b50c4a1d3ede7a9bcaa5f7898137118e3c42ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/validator/zipball/d6b50c4a1d3ede7a9bcaa5f7898137118e3c42ec", + "reference": "d6b50c4a1d3ede7a9bcaa5f7898137118e3c42ec", + "shasum": "" + }, + "require": { + "api-platform/metadata": "^3.4 || ^4.0", + "php": ">=8.1", + "symfony/web-link": "^6.4 || ^7.1" + }, + "require-dev": { + "phpspec/prophecy-phpunit": "^2.0", + "sebastian/comparator": "<5.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Validator\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "description": "API Platform validator component", + "homepage": "https://api-platform.com", + "keywords": [ + "api", + "graphql", + "rest", + "validator" + ], + "support": { + "source": "https://github.com/api-platform/validator/tree/v3.4.16" + }, + "time": "2024-10-30T09:51:13+00:00" }, { "name": "beberlei/assert", @@ -1748,6 +2694,97 @@ ], "time": "2024-04-18T06:56:21+00:00" }, + { + "name": "doctrine/common", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/d9ea4a54ca2586db781f0265d36bea731ac66ec5", + "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0 || ^10.0", + "doctrine/collections": "^1", + "phpstan/phpstan": "^1.4.1", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^6.1", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/common/issues", + "source": "https://github.com/doctrine/common/tree/3.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "type": "tidelift" + } + ], + "time": "2025-01-01T22:12:03+00:00" + }, { "name": "doctrine/data-fixtures", "version": "2.0.2", @@ -16590,12 +17627,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "70eb886a27427421cf1bd612067810c9fb1cbb5c" + "reference": "23b2141a1db97b4e3278510ed9e74a16361619b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/70eb886a27427421cf1bd612067810c9fb1cbb5c", - "reference": "70eb886a27427421cf1bd612067810c9fb1cbb5c", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/23b2141a1db97b4e3278510ed9e74a16361619b1", + "reference": "23b2141a1db97b4e3278510ed9e74a16361619b1", "shasum": "" }, "conflict": { @@ -16934,6 +17971,7 @@ "league/commonmark": "<2.6", "league/flysystem": "<1.1.4|>=2,<2.1.1", "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", + "leantime/leantime": "<3.3", "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", "libreform/libreform": ">=2,<=2.0.8", "librenms/librenms": "<2017.08.18", @@ -16958,7 +17996,7 @@ "mantisbt/mantisbt": "<=2.26.3", "marcwillmann/turn": "<0.3.3", "matyhtf/framework": "<3.0.6", - "mautic/core": "<4.4.13|>=5,<5.1.1", + "mautic/core": "<4.4.13|>=5.0.0.0-alpha,<5.1.1", "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", "maximebf/debugbar": "<1.19", "mdanter/ecc": "<2", @@ -17457,7 +18495,7 @@ "type": "tidelift" } ], - "time": "2025-02-18T20:05:22+00:00" + "time": "2025-02-21T23:05:15+00:00" }, { "name": "sebastian/cli-parser", diff --git a/src/ApiResource/.gitignore b/src/ApiResource/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/symfony.lock b/symfony.lock index c7471b73..5be0eea3 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,6 +13,20 @@ "src/ApiResource/.gitignore" ] }, + "api-platform/symfony": { + "version": "3.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "74b45ac570c57eb1fbe56c984091a9ff87e18bab" + }, + "files": [ + "./config/packages/api_platform.yaml", + "./config/routes/api_platform.yaml", + "./src/ApiResource/.gitignore" + ] + }, "beberlei/assert": { "version": "v3.2.6" }, From a54c2db9b932ebcd8ecff0d09326bb6cd20fb6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 19:59:12 +0100 Subject: [PATCH 09/18] Fixed type error introduced with api-platform upgrade --- .../APIPlatform/DetermineTypeFromElementIRIDenormalizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php b/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php index 8283dbbe..60508850 100644 --- a/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php +++ b/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace App\Serializer\APIPlatform; use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException; -use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -60,7 +60,6 @@ class DetermineTypeFromElementIRIDenormalizer implements DenormalizerInterface, * @param array $input * @param Operation $operation * @return array - * @throws ResourceClassNotFoundException */ private function addTypeDiscriminatorIfNecessary(array $input, Operation $operation): array { @@ -81,6 +80,7 @@ class DetermineTypeFromElementIRIDenormalizer implements DenormalizerInterface, } //Retrieve the element + //@phpstan-ignore-next-line $element = $this->iriConverter->getResourceFromIri($input['element']); //Retrieve the short name of the operation From cb0817666d0ca19b6cd9c4f3b0d36fcff820ccd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 21:12:54 +0100 Subject: [PATCH 10/18] Revert "Use the modular api-platform packages instead of the monolitic api-platform/core package" This reverts commit 17caf476bf5c2c6b657f5d9ecd53896667b4b8ea. --- composer.json | 4 +- composer.lock | 1324 ++++-------------------------------- src/ApiResource/.gitignore | 0 symfony.lock | 14 - 4 files changed, 144 insertions(+), 1198 deletions(-) delete mode 100644 src/ApiResource/.gitignore diff --git a/composer.json b/composer.json index 94a91a0a..3b2904d6 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,7 @@ "ext-json": "*", "ext-mbstring": "*", "amphp/http-client": "^5.1", - "api-platform/doctrine-orm": "^3", - "api-platform/json-api": "^3.4", - "api-platform/symfony": "^3", + "api-platform/core": "^3.1", "beberlei/doctrineextensions": "^1.2", "brick/math": "0.12.1 as 0.11.0", "composer/ca-bundle": "^1.3", diff --git a/composer.lock b/composer.lock index e4d3b662..e2b6eb60 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8f14b530c4184d1e512e7d1687c71e5", + "content-hash": "75643d42e05fce4684644d375bff2d0a", "packages": [ { "name": "amphp/amp", @@ -967,1127 +967,179 @@ "time": "2024-08-03T19:31:26+00:00" }, { - "name": "api-platform/doctrine-common", + "name": "api-platform/core", "version": "v3.4.16", "source": { "type": "git", - "url": "https://github.com/api-platform/doctrine-common.git", - "reference": "07c8dbca43861691d86c84b627870acf61976a74" + "url": "https://github.com/api-platform/core.git", + "reference": "64c6e1092cf988ba619907b3e4cce8a229ce4fae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/07c8dbca43861691d86c84b627870acf61976a74", - "reference": "07c8dbca43861691d86c84b627870acf61976a74", - "shasum": "" - }, - "require": { - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "doctrine/collections": "^2.1", - "doctrine/common": "^3.2.2", - "doctrine/persistence": "^3.2", - "php": ">=8.1" - }, - "conflict": { - "doctrine/persistence": "<1.3" - }, - "require-dev": { - "doctrine/mongodb-odm": "^2.6", - "doctrine/orm": "^2.17 || ^3.0", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^10.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0" - }, - "suggest": { - "api-platform/graphql": "For GraphQl mercure subscriptions.", - "api-platform/http-cache": "For HTTP cache invalidation.", - "phpstan/phpdoc-parser": "For PHP documentation support.", - "symfony/config": "For XML resource configuration.", - "symfony/mercure-bundle": "For mercure updates publisher.", - "symfony/messenger": "For async mercure updates.", - "symfony/yaml": "For YAML resource configuration." - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\Doctrine\\Common\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "Common files used by api-platform/doctrine-orm and api-platform/doctrine-odm", - "homepage": "https://api-platform.com", - "keywords": [ - "doctrine", - "graphql", - "odm", - "orm", - "rest" - ], - "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v3.4.16" - }, - "time": "2024-09-21T10:54:56+00:00" - }, - { - "name": "api-platform/doctrine-orm", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "01d49d6f89060eaf4804b02bc0f8d04f59107999" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/01d49d6f89060eaf4804b02bc0f8d04f59107999", - "reference": "01d49d6f89060eaf4804b02bc0f8d04f59107999", - "shasum": "" - }, - "require": { - "api-platform/doctrine-common": "^3.4 || ^4.0", - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "doctrine/orm": "^2.17 || ^3.0", - "php": ">=8.1", - "symfony/property-info": "^6.4 || ^7.1" - }, - "require-dev": { - "api-platform/parameter-validator": "^3.2", - "doctrine/doctrine-bundle": "^2.11", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^10.0", - "ramsey/uuid": "^4.7", - "ramsey/uuid-doctrine": "^2.0", - "symfony/cache": "^6.4 || ^7.0", - "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "symfony/property-access": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", - "symfony/uid": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\Doctrine\\Orm\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "Doctrine ORM bridge", - "homepage": "https://api-platform.com", - "keywords": [ - "api", - "doctrine", - "graphql", - "orm", - "rest" - ], - "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v3.4.16" - }, - "time": "2024-12-09T10:30:13+00:00" - }, - { - "name": "api-platform/documentation", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/documentation.git", - "reference": "056f61476ec408aef16c9aca514e2b9514fa8f1e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/documentation/zipball/056f61476ec408aef16c9aca514e2b9514fa8f1e", - "reference": "056f61476ec408aef16c9aca514e2b9514fa8f1e", - "shasum": "" - }, - "require": { - "api-platform/metadata": "^3.4 || ^4.0" - }, - "type": "project", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\Documentation\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "API Platform documentation controller.", - "support": { - "source": "https://github.com/api-platform/documentation/tree/v3.4.16" - }, - "time": "2024-09-21T10:54:56+00:00" - }, - { - "name": "api-platform/http-cache", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/http-cache.git", - "reference": "66abd8e964f4d5ce1f91802d0ec2b1938abd0fab" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/http-cache/zipball/66abd8e964f4d5ce1f91802d0ec2b1938abd0fab", - "reference": "66abd8e964f4d5ce1f91802d0ec2b1938abd0fab", - "shasum": "" - }, - "require": { - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "php": ">=8.1", - "symfony/http-foundation": "^6.4 || ^7.1" - }, - "require-dev": { - "guzzlehttp/guzzle": "^6.0 || ^7.0", - "phpspec/prophecy-phpunit": "^2.0", - "sebastian/comparator": "<5.0", - "symfony/dependency-injection": "^6.4 || ^7.0", - "symfony/http-client": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\HttpCache\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/comunnity/contributors" - } - ], - "description": "API Platform HttpCache component", - "homepage": "https://api-platform.com", - "keywords": [ - "api", - "cache", - "http", - "rest" - ], - "support": { - "source": "https://github.com/api-platform/http-cache/tree/v3.4.16" - }, - "time": "2024-10-26T07:09:46+00:00" - }, - { - "name": "api-platform/hydra", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/hydra.git", - "reference": "db4a2c416ebd749864bd116c87b9db566adb3894" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/hydra/zipball/db4a2c416ebd749864bd116c87b9db566adb3894", - "reference": "db4a2c416ebd749864bd116c87b9db566adb3894", - "shasum": "" - }, - "require": { - "api-platform/documentation": "^3.4 || ^4.0", - "api-platform/json-schema": "^3.4 || ^4.0", - "api-platform/jsonld": "^3.4 || ^4.0", - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/serializer": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "php": ">=8.1" - }, - "require-dev": { - "api-platform/doctrine-common": "^3.4 || ^4.0", - "api-platform/doctrine-odm": "^3.4 || ^4.0", - "api-platform/doctrine-orm": "^3.4 || ^4.0" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\Hydra\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "API Hydra support", - "homepage": "https://api-platform.com", - "keywords": [ - "Hydra", - "JSON-LD", - "api", - "graphql", - "jsonapi", - "rest" - ], - "support": { - "source": "https://github.com/api-platform/hydra/tree/v3.4.16" - }, - "time": "2024-10-25T09:44:09+00:00" - }, - { - "name": "api-platform/json-api", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/json-api.git", - "reference": "6f86528eddc9150e8e0c7a7dbacbca679de81cb6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-api/zipball/6f86528eddc9150e8e0c7a7dbacbca679de81cb6", - "reference": "6f86528eddc9150e8e0c7a7dbacbca679de81cb6", - "shasum": "" - }, - "require": { - "api-platform/documentation": "^3.4 || ^4.0", - "api-platform/json-schema": "^3.4 || ^4.0", - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/serializer": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "php": ">=8.1", - "symfony/error-handler": "^6.4 || ^7.1", - "symfony/http-foundation": "^6.4 || ^7.1" - }, - "require-dev": { - "phpspec/prophecy": "^1.19", - "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\JsonApi\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "API JSON-API support", - "homepage": "https://api-platform.com", - "keywords": [ - "api", - "jsonapi", - "rest" - ], - "support": { - "source": "https://github.com/api-platform/json-api/tree/v3.4.16" - }, - "time": "2024-12-13T10:57:36+00:00" - }, - { - "name": "api-platform/json-schema", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/json-schema.git", - "reference": "a1dff7eebefd3a64799418c8a2cc40fff4ab42fc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-schema/zipball/a1dff7eebefd3a64799418c8a2cc40fff4ab42fc", - "reference": "a1dff7eebefd3a64799418c8a2cc40fff4ab42fc", - "shasum": "" - }, - "require": { - "api-platform/metadata": "^3.4 || ^4.0", - "php": ">=8.1", - "symfony/console": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", - "symfony/uid": "^6.4 || ^7.0" - }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "sebastian/comparator": "<5.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\JsonSchema\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "Generate a JSON Schema from a PHP class", - "homepage": "https://api-platform.com", - "keywords": [ - "JSON Schema", - "api", - "json", - "openapi", - "rest", - "swagger" - ], - "support": { - "source": "https://github.com/api-platform/json-schema/tree/v3.4.16" - }, - "time": "2025-01-16T07:34:23+00:00" - }, - { - "name": "api-platform/jsonld", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/jsonld.git", - "reference": "aab1d012fb19e00ced5f5deac5928b5fa5aadc23" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/jsonld/zipball/aab1d012fb19e00ced5f5deac5928b5fa5aadc23", - "reference": "aab1d012fb19e00ced5f5deac5928b5fa5aadc23", - "shasum": "" - }, - "require": { - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/serializer": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\JsonLd\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "API JSON-LD support", - "homepage": "https://api-platform.com", - "keywords": [ - "Hydra", - "JSON-LD", - "api", - "graphql", - "rest" - ], - "support": { - "source": "https://github.com/api-platform/jsonld/tree/v3.4.16" - }, - "time": "2024-11-29T14:41:22+00:00" - }, - { - "name": "api-platform/metadata", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/metadata.git", - "reference": "931ccaf7a8df428ad5b18ae8a78b3266f41fec6f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/931ccaf7a8df428ad5b18ae8a78b3266f41fec6f", - "reference": "931ccaf7a8df428ad5b18ae8a78b3266f41fec6f", + "url": "https://api.github.com/repos/api-platform/core/zipball/64c6e1092cf988ba619907b3e4cce8a229ce4fae", + "reference": "64c6e1092cf988ba619907b3e4cce8a229ce4fae", "shasum": "" }, "require": { "doctrine/inflector": "^1.0 || ^2.0", "php": ">=8.1", "psr/cache": "^1.0 || ^2.0 || ^3.0", - "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/string": "^6.4 || ^7.0" - }, - "require-dev": { - "api-platform/json-schema": "^3.4 || ^4.0", - "api-platform/openapi": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpdoc-parser": "^1.16", - "sebastian/comparator": "<5.0", - "symfony/config": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "symfony/routing": "^6.4 || ^7.0", - "symfony/var-dumper": "^6.4 || ^7.0", - "symfony/web-link": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0" - }, - "suggest": { - "phpstan/phpdoc-parser": "For PHP documentation support.", - "symfony/config": "For XML resource configuration.", - "symfony/yaml": "For YAML resource configuration." - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\Metadata\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "API Resource-oriented metadata attributes and factories", - "homepage": "https://api-platform.com", - "keywords": [ - "Hydra", - "JSON-LD", - "api", - "graphql", - "hal", - "jsonapi", - "openapi", - "rest", - "swagger" - ], - "support": { - "source": "https://github.com/api-platform/metadata/tree/v3.4.16" - }, - "time": "2025-01-10T10:33:02+00:00" - }, - { - "name": "api-platform/openapi", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/openapi.git", - "reference": "3313d511f5257a1f0ed1929e7d809d02d63d90cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/3313d511f5257a1f0ed1929e7d809d02d63d90cf", - "reference": "3313d511f5257a1f0ed1929e7d809d02d63d90cf", - "shasum": "" - }, - "require": { - "api-platform/json-schema": "^3.4 || ^4.0", - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "php": ">=8.1", - "symfony/console": "^6.4 || ^7.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^3.1", + "symfony/http-foundation": "^6.4 || ^7.1", + "symfony/http-kernel": "^6.4 || ^7.1", "symfony/property-access": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1" + "symfony/property-info": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", + "symfony/translation-contracts": "^3.3", + "symfony/web-link": "^6.4 || ^7.1", + "willdurand/negotiation": "^3.0" + }, + "conflict": { + "doctrine/common": "<3.2.2", + "doctrine/dbal": "<2.10", + "doctrine/mongodb-odm": "<2.4", + "doctrine/orm": "<2.14.0", + "doctrine/persistence": "<1.3", + "elasticsearch/elasticsearch": ">=8.0,<8.4", + "phpspec/prophecy": "<1.15", + "phpunit/phpunit": "<9.5", + "symfony/framework-bundle": "6.4.6 || 7.0.6", + "symfony/var-exporter": "<6.1.1" + }, + "replace": { + "api-platform/doctrine-common": "self.version", + "api-platform/doctrine-odm": "self.version", + "api-platform/doctrine-orm": "self.version", + "api-platform/documentation": "self.version", + "api-platform/elasticsearch": "self.version", + "api-platform/graphql": "self.version", + "api-platform/http-cache": "self.version", + "api-platform/hydra": "self.version", + "api-platform/json-api": "self.version", + "api-platform/json-hal": "self.version", + "api-platform/json-schema": "self.version", + "api-platform/jsonld": "self.version", + "api-platform/laravel": "self.version", + "api-platform/metadata": "self.version", + "api-platform/openapi": "self.version", + "api-platform/parameter-validator": "self.version", + "api-platform/ramsey-uuid": "self.version", + "api-platform/serializer": "self.version", + "api-platform/state": "self.version", + "api-platform/symfony": "self.version", + "api-platform/validator": "self.version" }, "require-dev": { "api-platform/doctrine-common": "^3.4 || ^4.0", "api-platform/doctrine-odm": "^3.4 || ^4.0", "api-platform/doctrine-orm": "^3.4 || ^4.0", - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\OpenApi\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "Models to build and serialize an OpenAPI specification.", - "homepage": "https://api-platform.com", - "keywords": [ - "Hydra", - "JSON-LD", - "api", - "graphql", - "hal", - "jsonapi", - "openapi", - "rest", - "swagger" - ], - "support": { - "source": "https://github.com/api-platform/openapi/tree/v3.4.16" - }, - "time": "2025-01-09T10:29:41+00:00" - }, - { - "name": "api-platform/serializer", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/serializer.git", - "reference": "acc3d2b59ac79e6f3284b01c03c7573efc61ddf1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/serializer/zipball/acc3d2b59ac79e6f3284b01c03c7573efc61ddf1", - "reference": "acc3d2b59ac79e6f3284b01c03c7573efc61ddf1", - "shasum": "" - }, - "require": { - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "php": ">=8.1", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", - "symfony/validator": "^6.4 || ^7.0" - }, - "require-dev": { - "doctrine/collections": "^2.1", - "phpspec/prophecy-phpunit": "^2.0", - "sebastian/comparator": "<5.0", - "symfony/mercure-bundle": "*", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "symfony/var-dumper": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0" - }, - "suggest": { - "api-platform/doctrine-odm": "To support Doctrine MongoDB ODM state options.", - "api-platform/doctrine-orm": "To support Doctrine ORM state options." - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\Serializer\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "API Platform core Serializer", - "homepage": "https://api-platform.com", - "keywords": [ - "api", - "graphql", - "rest", - "serializer" - ], - "support": { - "source": "https://github.com/api-platform/serializer/tree/v3.4.16" - }, - "time": "2024-11-27T16:23:42+00:00" - }, - { - "name": "api-platform/state", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/state.git", - "reference": "b4a3e6f5e6e4bf1c9301b05cc2a08ddf5a6de94a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/state/zipball/b4a3e6f5e6e4bf1c9301b05cc2a08ddf5a6de94a", - "reference": "b4a3e6f5e6e4bf1c9301b05cc2a08ddf5a6de94a", - "shasum": "" - }, - "require": { - "api-platform/metadata": "^3.4 || ^4.0", - "php": ">=8.1", - "psr/container": "^1.0 || ^2.0", - "symfony/http-kernel": "^6.4 || ^7.0" - }, - "require-dev": { - "api-platform/serializer": "^3.4 || ^4.0", - "api-platform/validator": "^3.4 || ^4.0", - "phpunit/phpunit": "^10.3", - "symfony/http-foundation": "^6.4 || ^7.0", - "symfony/web-link": "^6.4 || ^7.0", - "willdurand/negotiation": "^3.1" - }, - "suggest": { - "api-platform/serializer": "To use API Platform serializer.", - "api-platform/validator": "To use API Platform validation.", - "symfony/http-foundation": "To use our HTTP providers and processor.", - "symfony/web-link": "To support adding web links to the response headers.", - "willdurand/negotiation": "To use the API Platform content negoatiation provider." - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\State\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "API Platform State component ", - "homepage": "https://api-platform.com", - "keywords": [ - "Hydra", - "JSON-LD", - "api", - "graphql", - "hal", - "jsonapi", - "openapi", - "rest", - "swagger" - ], - "support": { - "source": "https://github.com/api-platform/state/tree/v3.4.16" - }, - "time": "2024-12-06T10:05:08+00:00" - }, - { - "name": "api-platform/symfony", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/symfony.git", - "reference": "b44c6e609850fafac719855fc9f83392dbd27825" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/b44c6e609850fafac719855fc9f83392dbd27825", - "reference": "b44c6e609850fafac719855fc9f83392dbd27825", - "shasum": "" - }, - "require": { "api-platform/documentation": "^3.4 || ^4.0", + "api-platform/elasticsearch": "^3.4 || ^4.0", + "api-platform/graphql": "^3.4 || ^4.0", "api-platform/http-cache": "^3.4 || ^4.0", "api-platform/hydra": "^3.4 || ^4.0", + "api-platform/json-api": "^3.3 || ^4.0", "api-platform/json-schema": "^3.4 || ^4.0", "api-platform/jsonld": "^3.4 || ^4.0", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/openapi": "^3.4 || ^4.0", + "api-platform/parameter-validator": "^3.4", + "api-platform/ramsey-uuid": "^3.4 || ^4.0", "api-platform/serializer": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", "api-platform/validator": "^3.4 || ^4.0", - "php": ">=8.1", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/security-core": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.1", - "willdurand/negotiation": "^3.0" - }, - "require-dev": { - "api-platform/doctrine-common": "^3.4 || ^4.0", - "api-platform/doctrine-odm": "^3.4 || ^4.0", - "api-platform/doctrine-orm": "^3.4 || ^4.0", - "api-platform/elasticsearch": "^3.4 || ^4.0", - "api-platform/graphql": "^3.4 || ^4.0", - "api-platform/parameter-validator": "^3.1", + "behat/behat": "^3.11", + "behat/mink": "^1.9", + "doctrine/cache": "^1.11 || ^2.1", + "doctrine/common": "^3.2.2", + "doctrine/dbal": "^3.4.0 || ^4.0", + "doctrine/doctrine-bundle": "^1.12 || ^2.0", + "doctrine/mongodb-odm": "^2.2", + "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", + "doctrine/orm": "^2.14 || ^3.0", + "elasticsearch/elasticsearch": "^7.11 || ^8.4", + "friends-of-behat/mink-browserkit-driver": "^1.3.1", + "friends-of-behat/mink-extension": "^2.2", + "friends-of-behat/symfony-extension": "^2.1", + "guzzlehttp/guzzle": "^6.0 || ^7.1", + "jangregor/phpstan-prophecy": "^1.0", + "justinrainbow/json-schema": "^5.2.1", "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpdoc-parser": "^1.13|^2.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-symfony": "^1.0", + "phpunit/phpunit": "^9.6", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^3.9.7 || ^4.0", + "ramsey/uuid-doctrine": "^1.4 || ^2.0 || ^3.0", "sebastian/comparator": "<5.0", + "soyuka/contexts": "v3.3.9", + "soyuka/pmu": "^0.0.12", + "soyuka/stubs-mongodb": "^1.0", + "symfony/asset": "^6.4 || ^7.1", + "symfony/browser-kit": "^6.4 || ^7.1", + "symfony/cache": "^6.4 || ^7.1", + "symfony/config": "^6.4 || ^7.1", + "symfony/console": "^6.4 || ^7.1", + "symfony/css-selector": "^6.4 || ^7.1", + "symfony/dependency-injection": "^6.4 || ^7.1", + "symfony/doctrine-bridge": "^6.4 || ^7.1", + "symfony/dom-crawler": "^6.4 || ^7.1", + "symfony/error-handler": "^6.4 || ^7.1", + "symfony/event-dispatcher": "^6.4 || ^7.1", "symfony/expression-language": "^6.4 || ^7.1", + "symfony/finder": "^6.4 || ^7.1", + "symfony/form": "^6.4 || ^7.1", + "symfony/framework-bundle": "^6.4 || ^7.1", + "symfony/http-client": "^6.4 || ^7.1", + "symfony/intl": "^6.4 || ^7.1", + "symfony/maker-bundle": "^1.24", "symfony/mercure-bundle": "*", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "symfony/routing": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0", + "symfony/messenger": "^6.4 || ^7.1", + "symfony/phpunit-bridge": "^6.4.1 || ^7.1", + "symfony/routing": "^6.4 || ^7.1", + "symfony/security-bundle": "^6.4 || ^7.1", + "symfony/security-core": "^6.4 || ^7.1", + "symfony/stopwatch": "^6.4 || ^7.1", + "symfony/string": "^6.4 || ^7.1", + "symfony/twig-bundle": "^6.4 || ^7.1", + "symfony/uid": "^6.4 || ^7.1", + "symfony/validator": "^6.4 || ^7.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.1", + "symfony/yaml": "^6.4 || ^7.1", + "twig/twig": "^1.42.3 || ^2.12 || ^3.0", "webonyx/graphql-php": "^14.0 || ^15.0" }, "suggest": { - "api-platform/doctrine-odm": "To support MongoDB. Only versions 4.0 and later are supported.", - "api-platform/doctrine-orm": "To support Doctrine ORM.", - "api-platform/elasticsearch": "To support Elasticsearch.", - "api-platform/graphql": "To support GraphQL.", - "api-platform/ramsey-uuid": "To support Ramsey's UUID identifiers.", + "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", + "elasticsearch/elasticsearch": "To support Elasticsearch.", "ocramius/package-versions": "To display the API Platform's version in the debug bar.", "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", "psr/cache-implementation": "To use metadata caching.", + "ramsey/uuid": "To support Ramsey's UUID identifiers.", "symfony/cache": "To have metadata caching when using Symfony integration.", "symfony/config": "To load XML configuration files.", - "symfony/expression-language": "To use authorization and mercure advanced features.", + "symfony/expression-language": "To use authorization features.", "symfony/http-client": "To use the HTTP cache invalidation system.", - "symfony/mercure-bundle": "To support mercure integration.", - "symfony/messenger": "To support messenger integration and asynchronous Mercure updates.", + "symfony/messenger": "To support messenger integration.", "symfony/security": "To use authorization features.", "symfony/twig-bundle": "To use the Swagger UI integration.", "symfony/uid": "To support Symfony UUID/ULID identifiers.", - "symfony/web-profiler-bundle": "To use the data collector." - }, - "type": "symfony-bundle", - "extra": { - "thanks": { - "url": "https://github.com/api-platform/api-platform", - "name": "api-platform/api-platform" - }, - "symfony": { - "require": "^6.4 || ^7.1" - }, - "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-main": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "ApiPlatform\\Symfony\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "description": "Symfony API Platform integration", - "homepage": "https://api-platform.com", - "keywords": [ - "api", - "graphql", - "rest", - "symfony" - ], - "support": { - "source": "https://github.com/api-platform/symfony/tree/v3.4.16" - }, - "time": "2025-01-17T14:14:18+00:00" - }, - { - "name": "api-platform/validator", - "version": "v3.4.16", - "source": { - "type": "git", - "url": "https://github.com/api-platform/validator.git", - "reference": "d6b50c4a1d3ede7a9bcaa5f7898137118e3c42ec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/validator/zipball/d6b50c4a1d3ede7a9bcaa5f7898137118e3c42ec", - "reference": "d6b50c4a1d3ede7a9bcaa5f7898137118e3c42ec", - "shasum": "" - }, - "require": { - "api-platform/metadata": "^3.4 || ^4.0", - "php": ">=8.1", - "symfony/web-link": "^6.4 || ^7.1" - }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "sebastian/comparator": "<5.0", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0" + "symfony/web-profiler-bundle": "To use the data collector.", + "webonyx/graphql-php": "To support GraphQL." }, "type": "library", "extra": { + "pmu": { + "projects": [ + "./src/*/composer.json", + "src/Doctrine/*/composer.json" + ] + }, "thanks": { "url": "https://github.com/api-platform/api-platform", "name": "api-platform/api-platform" @@ -2102,7 +1154,7 @@ }, "autoload": { "psr-4": { - "ApiPlatform\\Validator\\": "" + "ApiPlatform\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2114,24 +1166,26 @@ "name": "Kévin Dunglas", "email": "kevin@dunglas.fr", "homepage": "https://dunglas.fr" - }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" } ], - "description": "API Platform validator component", + "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", "homepage": "https://api-platform.com", "keywords": [ + "Hydra", + "JSON-LD", "api", "graphql", + "hal", + "jsonapi", + "openapi", "rest", - "validator" + "swagger" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v3.4.16" + "issues": "https://github.com/api-platform/core/issues", + "source": "https://github.com/api-platform/core/tree/v3.4.16" }, - "time": "2024-10-30T09:51:13+00:00" + "time": "2025-01-17T14:17:26+00:00" }, { "name": "beberlei/assert", @@ -2694,97 +1748,6 @@ ], "time": "2024-04-18T06:56:21+00:00" }, - { - "name": "doctrine/common", - "version": "3.5.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/common.git", - "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/d9ea4a54ca2586db781f0265d36bea731ac66ec5", - "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5", - "shasum": "" - }, - "require": { - "doctrine/persistence": "^2.0 || ^3.0 || ^4.0", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0 || ^10.0", - "doctrine/collections": "^1", - "phpstan/phpstan": "^1.4.1", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.0", - "symfony/phpunit-bridge": "^6.1", - "vimeo/psalm": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - } - ], - "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", - "homepage": "https://www.doctrine-project.org/projects/common.html", - "keywords": [ - "common", - "doctrine", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.5.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", - "type": "tidelift" - } - ], - "time": "2025-01-01T22:12:03+00:00" - }, { "name": "doctrine/data-fixtures", "version": "2.0.2", @@ -17627,12 +16590,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "23b2141a1db97b4e3278510ed9e74a16361619b1" + "reference": "70eb886a27427421cf1bd612067810c9fb1cbb5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/23b2141a1db97b4e3278510ed9e74a16361619b1", - "reference": "23b2141a1db97b4e3278510ed9e74a16361619b1", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/70eb886a27427421cf1bd612067810c9fb1cbb5c", + "reference": "70eb886a27427421cf1bd612067810c9fb1cbb5c", "shasum": "" }, "conflict": { @@ -17971,7 +16934,6 @@ "league/commonmark": "<2.6", "league/flysystem": "<1.1.4|>=2,<2.1.1", "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", - "leantime/leantime": "<3.3", "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", "libreform/libreform": ">=2,<=2.0.8", "librenms/librenms": "<2017.08.18", @@ -17996,7 +16958,7 @@ "mantisbt/mantisbt": "<=2.26.3", "marcwillmann/turn": "<0.3.3", "matyhtf/framework": "<3.0.6", - "mautic/core": "<4.4.13|>=5.0.0.0-alpha,<5.1.1", + "mautic/core": "<4.4.13|>=5,<5.1.1", "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", "maximebf/debugbar": "<1.19", "mdanter/ecc": "<2", @@ -18495,7 +17457,7 @@ "type": "tidelift" } ], - "time": "2025-02-21T23:05:15+00:00" + "time": "2025-02-18T20:05:22+00:00" }, { "name": "sebastian/cli-parser", diff --git a/src/ApiResource/.gitignore b/src/ApiResource/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/symfony.lock b/symfony.lock index 5be0eea3..c7471b73 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,20 +13,6 @@ "src/ApiResource/.gitignore" ] }, - "api-platform/symfony": { - "version": "3.4", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "main", - "version": "3.3", - "ref": "74b45ac570c57eb1fbe56c984091a9ff87e18bab" - }, - "files": [ - "./config/packages/api_platform.yaml", - "./config/routes/api_platform.yaml", - "./src/ApiResource/.gitignore" - ] - }, "beberlei/assert": { "version": "v3.2.6" }, From b38ef8ecea26c037b5d8a575c7b66b007a7e08fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 21:12:58 +0100 Subject: [PATCH 11/18] Revert "Fixed type error introduced with api-platform upgrade" This reverts commit a54c2db9b932ebcd8ecff0d09326bb6cd20fb6a0. --- .../APIPlatform/DetermineTypeFromElementIRIDenormalizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php b/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php index 60508850..8283dbbe 100644 --- a/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php +++ b/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace App\Serializer\APIPlatform; use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException; -use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -60,6 +60,7 @@ class DetermineTypeFromElementIRIDenormalizer implements DenormalizerInterface, * @param array $input * @param Operation $operation * @return array + * @throws ResourceClassNotFoundException */ private function addTypeDiscriminatorIfNecessary(array $input, Operation $operation): array { @@ -80,7 +81,6 @@ class DetermineTypeFromElementIRIDenormalizer implements DenormalizerInterface, } //Retrieve the element - //@phpstan-ignore-next-line $element = $this->iriConverter->getResourceFromIri($input['element']); //Retrieve the short name of the operation From c4ba28e3a06395f6a2c07a112dd49aa8f8729dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 22:19:38 +0100 Subject: [PATCH 12/18] Heavily refactored the property metadata attribute logic The new method is much more universal and fixes issue #862 --- ...cumentedAPIPropertiesJSONSchemaFactory.php | 116 ------------------ .../DocumentedAPIProperty.php | 55 ++++++++- .../PropertyMetadataFactory.php | 73 +++++++++++ .../PropertyNameCollectionFactory.php | 68 ++++++++++ src/Entity/Attachments/Attachment.php | 13 +- 5 files changed, 202 insertions(+), 123 deletions(-) delete mode 100644 src/ApiPlatform/AddDocumentedAPIPropertiesJSONSchemaFactory.php rename src/ApiPlatform/{ => DocumentedAPIProperties}/DocumentedAPIProperty.php (59%) create mode 100644 src/ApiPlatform/DocumentedAPIProperties/PropertyMetadataFactory.php create mode 100644 src/ApiPlatform/DocumentedAPIProperties/PropertyNameCollectionFactory.php diff --git a/src/ApiPlatform/AddDocumentedAPIPropertiesJSONSchemaFactory.php b/src/ApiPlatform/AddDocumentedAPIPropertiesJSONSchemaFactory.php deleted file mode 100644 index 07ff0f1b..00000000 --- a/src/ApiPlatform/AddDocumentedAPIPropertiesJSONSchemaFactory.php +++ /dev/null @@ -1,116 +0,0 @@ -. - */ - -declare(strict_types=1); - - -namespace App\ApiPlatform; - -use ApiPlatform\JsonSchema\Schema; -use ApiPlatform\JsonSchema\SchemaFactoryInterface; -use ApiPlatform\Metadata\Operation; -use Symfony\Component\DependencyInjection\Attribute\AsDecorator; - -/** - * This decorator adds the properties given by DocumentedAPIProperty attributes on the classes to the schema. - */ -#[AsDecorator('api_platform.json_schema.schema_factory')] -class AddDocumentedAPIPropertiesJSONSchemaFactory implements SchemaFactoryInterface -{ - - public function __construct(private readonly SchemaFactoryInterface $decorated) - { - } - - public function buildSchema( - string $className, - string $format = 'json', - string $type = Schema::TYPE_OUTPUT, - ?Operation $operation = null, - ?Schema $schema = null, - ?array $serializerContext = null, - bool $forceCollection = false - ): Schema { - - - $schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); - - //Check if there is are DocumentedAPIProperty attributes on the class - $reflectionClass = new \ReflectionClass($className); - $attributes = $reflectionClass->getAttributes(DocumentedAPIProperty::class); - foreach ($attributes as $attribute) { - /** @var DocumentedAPIProperty $api_property */ - $api_property = $attribute->newInstance(); - $this->addPropertyToSchema($schema, $api_property->schemaName, $api_property->property, - $api_property, $serializerContext ?? [], $format); - } - - return $schema; - } - - private function addPropertyToSchema(Schema $schema, string $definitionName, string $normalizedPropertyName, DocumentedAPIProperty $propertyMetadata, array $serializerContext, string $format): void - { - $version = $schema->getVersion(); - $swagger = Schema::VERSION_SWAGGER === $version; - - $propertySchema = []; - - if (false === $propertyMetadata->writeable) { - $propertySchema['readOnly'] = true; - } - if (!$swagger && false === $propertyMetadata->readable) { - $propertySchema['writeOnly'] = true; - } - if (null !== $description = $propertyMetadata->description) { - $propertySchema['description'] = $description; - } - - $deprecationReason = $propertyMetadata->deprecationReason; - - // see https://github.com/json-schema-org/json-schema-spec/pull/737 - if (!$swagger && null !== $deprecationReason) { - $propertySchema['deprecated'] = true; - } - - if (!empty($default = $propertyMetadata->default)) { - if ($default instanceof \BackedEnum) { - $default = $default->value; - } - $propertySchema['default'] = $default; - } - - if (!empty($example = $propertyMetadata->example)) { - $propertySchema['example'] = $example; - } - - if (!isset($propertySchema['example']) && isset($propertySchema['default'])) { - $propertySchema['example'] = $propertySchema['default']; - } - - $propertySchema['type'] = $propertyMetadata->type; - $propertySchema['nullable'] = $propertyMetadata->nullable; - - $propertySchema = new \ArrayObject($propertySchema); - - $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema; - } - - -} \ No newline at end of file diff --git a/src/ApiPlatform/DocumentedAPIProperty.php b/src/ApiPlatform/DocumentedAPIProperties/DocumentedAPIProperty.php similarity index 59% rename from src/ApiPlatform/DocumentedAPIProperty.php rename to src/ApiPlatform/DocumentedAPIProperties/DocumentedAPIProperty.php index c4c0a337..57d275be 100644 --- a/src/ApiPlatform/DocumentedAPIProperty.php +++ b/src/ApiPlatform/DocumentedAPIProperties/DocumentedAPIProperty.php @@ -21,7 +21,9 @@ declare(strict_types=1); -namespace App\ApiPlatform; +namespace App\ApiPlatform\DocumentedAPIProperties; + +use ApiPlatform\Metadata\ApiProperty; /** * When this attribute is applied to a class, an property will be added to the API documentation using the given parameters. @@ -64,4 +66,55 @@ final class DocumentedAPIProperty ) { } + + public function toAPIProperty(bool $use_swagger = false): ApiProperty + { + $openApiContext = []; + + if (false === $this->writeable) { + $openApiContext['readOnly'] = true; + } + if (!$use_swagger && false === $this->readable) { + $openApiContext['writeOnly'] = true; + } + if (null !== $description = $this->description) { + $openApiContext['description'] = $description; + } + + $deprecationReason = $this->deprecationReason; + + // see https://github.com/json-schema-org/json-schema-spec/pull/737 + if (!$use_swagger && null !== $deprecationReason) { + $openApiContext['deprecated'] = true; + } + + if (!empty($default = $this->default)) { + if ($default instanceof \BackedEnum) { + $default = $default->value; + } + $openApiContext['default'] = $default; + } + + if (!empty($example = $this->example)) { + $openApiContext['example'] = $example; + } + + if (!isset($openApiContext['example']) && isset($openApiContext['default'])) { + $openApiContext['example'] = $openApiContext['default']; + } + + $openApiContext['type'] = $this->type; + $openApiContext['nullable'] = $this->nullable; + + + + return new ApiProperty( + description: $this->description, + readable: $this->readable, + writable: $this->writeable, + openapiContext: $openApiContext, + types: $this->type, + property: $this->property + ); + } } \ No newline at end of file diff --git a/src/ApiPlatform/DocumentedAPIProperties/PropertyMetadataFactory.php b/src/ApiPlatform/DocumentedAPIProperties/PropertyMetadataFactory.php new file mode 100644 index 00000000..49e9a031 --- /dev/null +++ b/src/ApiPlatform/DocumentedAPIProperties/PropertyMetadataFactory.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\DocumentedAPIProperties; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ReflectionClass; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +/** + * This decorator adds the virtual properties defined by the DocumentedAPIProperty attribute to the property metadata + * which then get picked up by the openapi schema generator + */ +#[AsDecorator('api_platform.metadata.property.metadata_factory')] +class PropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + public function __construct(private PropertyMetadataFactoryInterface $decorated) + { + } + + public function create(string $resourceClass, string $property, array $options = []): ApiProperty + { + $metadata = $this->decorated->create($resourceClass, $property, $options); + + //Only become active in the context of the openapi schema generation + if (!isset($options['schema_type'])) { + return $metadata; + } + + if (!class_exists($resourceClass)) { + return $metadata; + } + + $refClass = new ReflectionClass($resourceClass); + $attributes = $refClass->getAttributes(DocumentedAPIProperty::class); + + //Look for the DocumentedAPIProperty attribute with the given property name + foreach ($attributes as $attribute) { + /** @var DocumentedAPIProperty $api_property */ + $api_property = $attribute->newInstance(); + //If attribute not matches the property name, skip it + if ($api_property->property !== $property) { + continue; + } + + //Return the virtual property + return $api_property->toAPIProperty(); + } + + return $metadata; + } +} \ No newline at end of file diff --git a/src/ApiPlatform/DocumentedAPIProperties/PropertyNameCollectionFactory.php b/src/ApiPlatform/DocumentedAPIProperties/PropertyNameCollectionFactory.php new file mode 100644 index 00000000..3157cbf3 --- /dev/null +++ b/src/ApiPlatform/DocumentedAPIProperties/PropertyNameCollectionFactory.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\DocumentedAPIProperties; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ReflectionClass; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +/** + * This decorator adds the virtual property names defined by the DocumentedAPIProperty attribute to the property name collection + * which then get picked up by the openapi schema generator + */ +#[AsDecorator('api_platform.metadata.property.name_collection_factory')] +class PropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface +{ + public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated) + { + } + + public function create(string $resourceClass, array $options = []): PropertyNameCollection + { + // Get the default properties from the decorated service + $propertyNames = $this->decorated->create($resourceClass, $options); + + //Only become active in the context of the openapi schema generation + if (!isset($options['schema_type'])) { + return $propertyNames; + } + + if (!class_exists($resourceClass)) { + return $propertyNames; + } + + $properties = iterator_to_array($propertyNames); + + $refClass = new ReflectionClass($resourceClass); + + foreach ($refClass->getAttributes(DocumentedAPIProperty::class) as $attribute) { + /** @var DocumentedAPIProperty $instance */ + $instance = $attribute->newInstance(); + $properties[] = $instance->property; + } + + return new PropertyNameCollection($properties); + } +} \ No newline at end of file diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 4847eea5..00cf581a 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -33,23 +33,24 @@ use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; -use App\ApiPlatform\DocumentedAPIProperty; +use App\ApiPlatform\DocumentedAPIProperties\DocumentedAPIProperty; use App\ApiPlatform\Filter\EntityFilter; use App\ApiPlatform\Filter\LikeFilter; use App\ApiPlatform\HandleAttachmentsUploadsProcessor; -use App\Repository\AttachmentRepository; -use App\EntityListeners\AttachmentDeleteListener; -use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractNamedDBElement; +use App\EntityListeners\AttachmentDeleteListener; +use App\Repository\AttachmentRepository; use App\Validator\Constraints\Selectable; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use InvalidArgumentException; +use LogicException; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Serializer\Attribute\DiscriminatorMap; use Symfony\Component\Validator\Constraints as Assert; + use function in_array; -use InvalidArgumentException; -use LogicException; /** * Class Attachment. From 319b69f6c73a23689270b6c454a0a92979b25bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 22:59:55 +0100 Subject: [PATCH 13/18] Added an workaround for issue #862 --- ...NormalizePropertyNameCollectionFactory.php | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/ApiPlatform/NormalizePropertyNameCollectionFactory.php diff --git a/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php b/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php new file mode 100644 index 00000000..33a4ef90 --- /dev/null +++ b/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php @@ -0,0 +1,77 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use function Symfony\Component\String\u; + +/** + * This decorator removes all camelCase property names from the property name collection, if a snake_case version exists. + * This is a fix for https://github.com/Part-DB/Part-DB-server/issues/862, as the openapi schema generator wrongly collects + * both camelCase and snake_case property names, which leads to duplicate properties in the schema. + * This seems to come from the fact that the openapi schema generator uses no serializerContext, which seems then to collect + * the getters too... + */ +#[AsDecorator('api_platform.metadata.property.name_collection_factory')] +class NormalizePropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface +{ + public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated) + { + } + + public function create(string $resourceClass, array $options = []): PropertyNameCollection + { + // Get the default properties from the decorated service + $propertyNames = $this->decorated->create($resourceClass, $options); + + //Only become active in the context of the openapi schema generation + if (!isset($options['schema_type'])) { + return $propertyNames; + } + + //If we are not in the jsonapi generator (which sets no serializer groups), return the property names as is + if (isset($options['serializer_groups'])) { + return $propertyNames; + } + + //Remove all camelCase property names from the collection, if a snake_case version exists + $properties = iterator_to_array($propertyNames); + + foreach ($properties as $property) { + if (str_contains($property, '_')) { + $camelized = u($property)->camel()->toString(); + + //If the camelized version exists, remove it from the collection + $index = array_search($camelized, $properties); + if ($index !== false) { + unset($properties[$index]); + } + } + } + + return new PropertyNameCollection($properties); + } +} \ No newline at end of file From 5d3f861728027eac8e00141f01a1a3fcf8f0fb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Feb 2025 23:29:57 +0100 Subject: [PATCH 14/18] Use newer version of farnell/element14 api to get the correct links to product pages. Also we can now retrieve a more detailed description, which will be put into the notes field --- .../Providers/Element14Provider.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/Element14Provider.php b/src/Services/InfoProviderSystem/Providers/Element14Provider.php index ad70f9d9..eb1d4675 100644 --- a/src/Services/InfoProviderSystem/Providers/Element14Provider.php +++ b/src/Services/InfoProviderSystem/Providers/Element14Provider.php @@ -35,7 +35,7 @@ class Element14Provider implements InfoProviderInterface { private const ENDPOINT_URL = 'https://api.element14.com/catalog/products'; - private const API_VERSION_NUMBER = '1.2'; + private const API_VERSION_NUMBER = '1.4'; private const NUMBER_OF_RESULTS = 20; public const DISTRIBUTOR_NAME = 'Farnell'; @@ -83,7 +83,7 @@ class Element14Provider implements InfoProviderInterface 'resultsSettings.responseGroup' => 'large', 'callInfo.apiKey' => $this->api_key, 'callInfo.responseDataFormat' => 'json', - 'callInfo.version' => self::API_VERSION_NUMBER, + 'versionNumber' => self::API_VERSION_NUMBER, ], ]); @@ -107,21 +107,18 @@ class Element14Provider implements InfoProviderInterface mpn: $product['translatedManufacturerPartNumber'], preview_image_url: $this->toImageUrl($product['image'] ?? null), manufacturing_status: $this->releaseStatusCodeToManufacturingStatus($product['releaseStatusCode'] ?? null), - provider_url: $this->generateProductURL($product['sku']), + provider_url: $product['productURL'], + notes: $product['productOverview']['description'] ?? null, datasheets: $this->parseDataSheets($product['datasheets'] ?? null), parameters: $this->attributesToParameters($product['attributes'] ?? null), - vendor_infos: $this->pricesToVendorInfo($product['sku'], $product['prices'] ?? []) + vendor_infos: $this->pricesToVendorInfo($product['sku'], $product['prices'] ?? [], $product['productURL']), + ); } return $result; } - private function generateProductURL($sku): string - { - return 'https://' . $this->store_id . '/' . $sku; - } - /** * @param array|null $datasheets * @return FileDTO[]|null Array of FileDTOs @@ -161,7 +158,7 @@ class Element14Provider implements InfoProviderInterface * @param array $prices * @return array */ - private function pricesToVendorInfo(string $sku, array $prices): array + private function pricesToVendorInfo(string $sku, array $prices, string $product_url): array { $price_dtos = []; @@ -179,7 +176,7 @@ class Element14Provider implements InfoProviderInterface distributor_name: self::DISTRIBUTOR_NAME, order_number: $sku, prices: $price_dtos, - product_url: $this->generateProductURL($sku) + product_url: $product_url ) ]; } From 0ba352ab0b2678d2a19ae92e4ad4df10cac0c594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 23 Feb 2025 17:28:39 +0100 Subject: [PATCH 15/18] Updated composer dependencies --- composer.lock | 11 ++++++----- yarn.lock | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/composer.lock b/composer.lock index e2b6eb60..6857910e 100644 --- a/composer.lock +++ b/composer.lock @@ -16590,12 +16590,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "70eb886a27427421cf1bd612067810c9fb1cbb5c" + "reference": "23b2141a1db97b4e3278510ed9e74a16361619b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/70eb886a27427421cf1bd612067810c9fb1cbb5c", - "reference": "70eb886a27427421cf1bd612067810c9fb1cbb5c", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/23b2141a1db97b4e3278510ed9e74a16361619b1", + "reference": "23b2141a1db97b4e3278510ed9e74a16361619b1", "shasum": "" }, "conflict": { @@ -16934,6 +16934,7 @@ "league/commonmark": "<2.6", "league/flysystem": "<1.1.4|>=2,<2.1.1", "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", + "leantime/leantime": "<3.3", "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", "libreform/libreform": ">=2,<=2.0.8", "librenms/librenms": "<2017.08.18", @@ -16958,7 +16959,7 @@ "mantisbt/mantisbt": "<=2.26.3", "marcwillmann/turn": "<0.3.3", "matyhtf/framework": "<3.0.6", - "mautic/core": "<4.4.13|>=5,<5.1.1", + "mautic/core": "<4.4.13|>=5.0.0.0-alpha,<5.1.1", "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", "maximebf/debugbar": "<1.19", "mdanter/ecc": "<2", @@ -17457,7 +17458,7 @@ "type": "tidelift" } ], - "time": "2025-02-18T20:05:22+00:00" + "time": "2025-02-21T23:05:15+00:00" }, { "name": "sebastian/cli-parser", diff --git a/yarn.lock b/yarn.lock index 7c0b8ac5..e87cf3c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2085,9 +2085,9 @@ integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/node@*": - version "22.13.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a" - integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg== + version "22.13.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.5.tgz#23add1d71acddab2c6a4d31db89c0f98d330b511" + integrity sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg== dependencies: undici-types "~6.20.0" @@ -2629,7 +2629,7 @@ cacache@^15.0.5: tar "^6.0.2" unique-filename "^1.1.1" -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -3501,9 +3501,9 @@ duplexer@^0.1.2: integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== electron-to-chromium@^1.5.73: - version "1.5.102" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz#81a452ace8e2c3fa7fba904ea4fed25052c53d3f" - integrity sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q== + version "1.5.103" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz#3d02025bc16e96e5edb3ed3ffa2538a11ae682dc" + integrity sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA== emoji-regex@^7.0.1: version "7.0.3" @@ -3639,7 +3639,7 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== -es-object-atoms@^1.0.0: +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== @@ -3933,16 +3933,16 @@ get-caller-file@^2.0.1: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044" - integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: - call-bind-apply-helpers "^1.0.1" + call-bind-apply-helpers "^1.0.2" es-define-property "^1.0.1" es-errors "^1.3.0" - es-object-atoms "^1.0.0" + es-object-atoms "^1.1.1" function-bind "^1.1.2" - get-proto "^1.0.0" + get-proto "^1.0.1" gopd "^1.2.0" has-symbols "^1.1.0" hasown "^2.0.2" From 6537502696cfbf39984dc44a9e9c8b8dc774afa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 23 Feb 2025 17:30:33 +0100 Subject: [PATCH 16/18] New Crowdin updates (#867) * New translations messages.en.xlf (English) * New translations messages.en.xlf (German) --- translations/messages.de.xlf | 56 ++++++++++++++++++--------- translations/messages.en.xlf | 74 ++++++++++++++++++------------------ 2 files changed, 76 insertions(+), 54 deletions(-) diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 4c24893c..b17243bc 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -779,18 +779,10 @@ Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müs Löschen - - - Part-DB1\templates\AdminPages\_attachments.html.twig:41 - Part-DB1\templates\Parts\edit\_attachments.html.twig:38 - Part-DB1\templates\Parts\info\_attachments_info.html.twig:35 - Part-DB1\src\DataTables\AttachmentDataTable.php:159 - Part-DB1\templates\Parts\edit\_attachments.html.twig:38 - Part-DB1\src\DataTables\AttachmentDataTable.php:159 - + - attachment.external - Extern + attachment.external_only + Nur Extern @@ -805,7 +797,7 @@ Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müs Thumbnail des Dateianhanges - + Part-DB1\templates\AdminPages\_attachments.html.twig:52 Part-DB1\templates\Parts\edit\_attachments.html.twig:50 @@ -815,8 +807,8 @@ Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müs Part-DB1\templates\Parts\info\_attachments_info.html.twig:45 - attachment.view - Anzeigen + attachment.view_local + Lokale Datei anzeigen @@ -2118,14 +2110,14 @@ Subelemente werden beim Löschen nach oben verschoben. Vorschaubild - + Part-DB1\templates\Parts\info\_attachments_info.html.twig:67 Part-DB1\templates\Parts\info\_attachments_info.html.twig:50 - attachment.download - Herunterladen + attachment.download_local + Lokale Datei downloaden @@ -12319,5 +12311,35 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Profil gespeichert! + + + entity.export.flash.error.no_entities + Es gibt keine Entitäten zu exportieren! + + + + + attachment.table.internal_file + Interne Datei + + + + + attachment.table.external_link + Externer link + + + + + attachment.view_external.view_at + Auf %host% anzeigen + + + + + attachment.view_external + Externe Version anzeigen + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 41388de3..276c49f4 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -242,7 +242,7 @@ part.info.timetravel_hint - Please note that this feature is experimental, so the info may not be correct.]]> + This is how the part appeared before %timestamp%. <i>Please note that this feature is experimental, so the info may not be correct.</i> @@ -731,10 +731,10 @@ user.edit.tfa.disable_tfa_message - all active two-factor authentication methods of the user and delete the backup codes! -
-The user will have to set up all two-factor authentication methods again and print new backup codes!

-Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!]]>
+ This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>! +<br> +The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br> +<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b>
@@ -885,9 +885,9 @@ The user will have to set up all two-factor authentication methods again and pri entity.delete.message - -Sub elements will be moved upwards.]]> + This can not be undone! +<br> +Sub elements will be moved upwards. @@ -1441,7 +1441,7 @@ Sub elements will be moved upwards.]]> homepage.github.text - GitHub project page]]> + Source, downloads, bug reports, to-do-list etc. can be found on <a href="%href%" class="link-external" target="_blank">GitHub project page</a> @@ -1463,7 +1463,7 @@ Sub elements will be moved upwards.]]> homepage.help.text - GitHub page]]> + Help and tips can be found in Wiki the <a href="%href%" class="link-external" target="_blank">GitHub page</a> @@ -1705,7 +1705,7 @@ Sub elements will be moved upwards.]]> email.pw_reset.fallback - %url% and enter the following info]]> + If this does not work for you, go to <a href="%url%">%url%</a> and enter the following info @@ -1735,7 +1735,7 @@ Sub elements will be moved upwards.]]> email.pw_reset.valid_unit %date% - %date%.]]> + The reset token will be valid until <i>%date%</i>. @@ -3578,8 +3578,8 @@ Sub elements will be moved upwards.]]> tfa_google.disable.confirm_message - -Also note that without two-factor authentication, your account is no longer as well protected against attackers!]]> + If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.<br> +Also note that without two-factor authentication, your account is no longer as well protected against attackers! @@ -3599,7 +3599,7 @@ Also note that without two-factor authentication, your account is no longer as w tfa_google.step.download - Google Authenticator oder FreeOTP Authenticator)]]> + Download an authenticator app (e.g. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> oder <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>) @@ -3841,8 +3841,8 @@ Also note that without two-factor authentication, your account is no longer as w tfa_trustedDevices.explanation - all computers here.]]> + When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed. +If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of <i>all </i>computers here. @@ -5313,7 +5313,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can label_options.lines_mode.help - Twig documentation and Wiki for more information.]]> + If you select Twig here, the content field is interpreted as Twig template. See <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig documentation</a> and <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a> for more information. @@ -9388,25 +9388,25 @@ Element 3 filter.parameter_value_constraint.operator.< - + Typ. Value < filter.parameter_value_constraint.operator.> - ]]> + Typ. Value > filter.parameter_value_constraint.operator.<= - + Typ. Value <= filter.parameter_value_constraint.operator.>= - =]]> + Typ. Value >= @@ -9514,7 +9514,7 @@ Element 3 parts_list.search.searching_for - %keyword%]]> + Searching parts with keyword <b>%keyword%</b> @@ -10174,13 +10174,13 @@ Element 3 project.builds.number_of_builds_possible - %max_builds% builds of this project.]]> + You have enough stocked to build <b>%max_builds%</b> builds of this project. project.builds.check_project_status - "%project_status%". You should check if you really want to build the project with this status!]]> + The current project status is <b>"%project_status%"</b>. You should check if you really want to build the project with this status! @@ -10282,7 +10282,7 @@ Element 3 entity.select.add_hint - to create nested structures, e.g. "Node 1->Node 1.1"]]> + Use -> to create nested structures, e.g. "Node 1->Node 1.1" @@ -10306,13 +10306,13 @@ Element 3 homepage.first_steps.introduction - documentation or start to creating the following data structures:]]> + Your database is still empty. You might want to read the <a href="%url%">documentation</a> or start to creating the following data structures: homepage.first_steps.create_part - create a new part.]]> + Or you can directly <a href="%url%">create a new part</a>. @@ -10324,7 +10324,7 @@ Element 3 homepage.forum.text - discussion forum]]> + For questions about Part-DB use the <a href="%href%" class="link-external" target="_blank">discussion forum</a> @@ -10978,7 +10978,7 @@ Element 3 parts.import.help_documentation - documentation for more information on the file format.]]> + See the <a href="%link%">documentation</a> for more information on the file format. @@ -11158,7 +11158,7 @@ Element 3 part.filter.lessThanDesired - + In stock less than desired (total amount < min. amount) @@ -11970,13 +11970,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g part.merge.confirm.title - %other% into %target%?]]> + Do you really want to merge <b>%other%</b> into <b>%target%</b>? part.merge.confirm.message - %other% will be deleted, and the part will be saved with the shown information.]]> + <b>%other%</b> will be deleted, and the part will be saved with the shown information. @@ -12322,25 +12322,25 @@ Please note, that you can not impersonate a disabled user. If you try you will g - + attachment.table.internal_file Internal file - + attachment.table.external_link External link - + attachment.view_external.view_at View at %host% - + attachment.view_external View external version From 2b5030c69f58a5e06f80d975e6595033c0cff9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 23 Feb 2025 22:40:02 +0100 Subject: [PATCH 17/18] Bumped version to 1.16.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 42cf0675..15b989e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.15.2 +1.16.0 From 1935258978afefee2183252c12e7e5d365f08fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 23 Feb 2025 22:55:59 +0100 Subject: [PATCH 18/18] Fixed phpstan issue --- src/ApiPlatform/NormalizePropertyNameCollectionFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php b/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php index 33a4ef90..c6a8220e 100644 --- a/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php +++ b/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php @@ -65,7 +65,7 @@ class NormalizePropertyNameCollectionFactory implements PropertyNameCollectionFa $camelized = u($property)->camel()->toString(); //If the camelized version exists, remove it from the collection - $index = array_search($camelized, $properties); + $index = array_search($camelized, $properties, true); if ($index !== false) { unset($properties[$index]); }