Skip to content

Commit bac94bd

Browse files
authored
Merge branch 'Part-DB:master' into master
2 parents c2e346c + 5d3f861 commit bac94bd

36 files changed

+898
-522
lines changed

config/packages/api_platform.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ api_platform:
3434

3535
keep_legacy_inflector: false
3636
# Need to be true, or some tests will fail
37-
use_symfony_listeners: true
37+
use_symfony_listeners: true
38+
39+
serializer:
40+
# Change this to false later, to remove the hydra prefix on the API
41+
hydra_prefix: true
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use App\Migration\AbstractMultiPlatformMigration;
8+
use Doctrine\DBAL\Schema\Schema;
9+
10+
final class Version20250220215048 extends AbstractMultiPlatformMigration
11+
{
12+
public function getDescription(): string
13+
{
14+
return 'Split $path property for attachments into $internal_path and $external_path';
15+
}
16+
17+
public function mySQLUp(Schema $schema): void
18+
{
19+
//Create the new columns as nullable (that is easier modifying them)
20+
$this->addSql('ALTER TABLE attachments ADD internal_path VARCHAR(255) DEFAULT NULL, ADD external_path VARCHAR(255) DEFAULT NULL');
21+
22+
//Copy the data from path to external_path and remove the path column
23+
$this->addSql('UPDATE attachments SET external_path=path');
24+
$this->addSql('ALTER TABLE attachments DROP path');
25+
26+
27+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\'');
28+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\'');
29+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\'');
30+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\'');
31+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\'');
32+
$this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL');
33+
}
34+
35+
public function mySQLDown(Schema $schema): void
36+
{
37+
$this->addSql('UPDATE attachments SET external_path=internal_path WHERE internal_path IS NOT NULL');
38+
$this->addSql('ALTER TABLE attachments DROP internal_path');
39+
$this->addSql('ALTER TABLE attachments RENAME COLUMN external_path TO path');
40+
}
41+
42+
public function postgreSQLUp(Schema $schema): void
43+
{
44+
//We can use the same SQL for PostgreSQL as for MySQL
45+
$this->mySQLUp($schema);
46+
}
47+
48+
public function postgreSQLDown(Schema $schema): void
49+
{
50+
//We can use the same SQL for PostgreSQL as for MySQL
51+
$this->mySQLDown($schema);
52+
}
53+
54+
public function sqLiteUp(Schema $schema): void
55+
{
56+
$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');
57+
$this->addSql('DROP TABLE attachments');
58+
$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)');
59+
$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');
60+
$this->addSql('DROP TABLE __temp__attachments');
61+
$this->addSql('CREATE INDEX attachment_element_idx ON attachments (class_name, element_id)');
62+
$this->addSql('CREATE INDEX attachment_name_idx ON attachments (name)');
63+
$this->addSql('CREATE INDEX attachments_idx_class_name_id ON attachments (class_name, id)');
64+
$this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON attachments (id, element_id, class_name)');
65+
$this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON attachments (type_id)');
66+
$this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON attachments (element_id)');
67+
68+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\'');
69+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\'');
70+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\'');
71+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\'');
72+
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\'');
73+
$this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL');
74+
}
75+
76+
public function sqLiteDown(Schema $schema): void
77+
{
78+
//Reuse the MySQL down migration:
79+
$this->mySQLDown($schema);
80+
}
81+
82+
83+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20250222165240 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Migrate the old attachment class discriminator values from legacy Part-DB to the modern format, so that there is just one unified value';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
//Change the old discriminator values to the new ones
23+
$this->addSql("UPDATE attachments SET class_name = 'Part' WHERE class_name = 'PartDB\Part'");
24+
$this->addSql("UPDATE attachments SET class_name = 'Device' WHERE class_name = 'PartDB\Device'");
25+
}
26+
27+
public function down(Schema $schema): void
28+
{
29+
//No down required, as the new format can also be read by older Part-DB version
30+
}
31+
}

src/ApiPlatform/AddDocumentedAPIPropertiesJSONSchemaFactory.php

Lines changed: 0 additions & 116 deletions
This file was deleted.

src/ApiPlatform/DocumentedAPIProperty.php renamed to src/ApiPlatform/DocumentedAPIProperties/DocumentedAPIProperty.php

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
declare(strict_types=1);
2222

2323

24-
namespace App\ApiPlatform;
24+
namespace App\ApiPlatform\DocumentedAPIProperties;
25+
26+
use ApiPlatform\Metadata\ApiProperty;
2527

2628
/**
2729
* 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 @@ public function __construct(
6466
)
6567
{
6668
}
69+
70+
public function toAPIProperty(bool $use_swagger = false): ApiProperty
71+
{
72+
$openApiContext = [];
73+
74+
if (false === $this->writeable) {
75+
$openApiContext['readOnly'] = true;
76+
}
77+
if (!$use_swagger && false === $this->readable) {
78+
$openApiContext['writeOnly'] = true;
79+
}
80+
if (null !== $description = $this->description) {
81+
$openApiContext['description'] = $description;
82+
}
83+
84+
$deprecationReason = $this->deprecationReason;
85+
86+
// see https://github.com/json-schema-org/json-schema-spec/pull/737
87+
if (!$use_swagger && null !== $deprecationReason) {
88+
$openApiContext['deprecated'] = true;
89+
}
90+
91+
if (!empty($default = $this->default)) {
92+
if ($default instanceof \BackedEnum) {
93+
$default = $default->value;
94+
}
95+
$openApiContext['default'] = $default;
96+
}
97+
98+
if (!empty($example = $this->example)) {
99+
$openApiContext['example'] = $example;
100+
}
101+
102+
if (!isset($openApiContext['example']) && isset($openApiContext['default'])) {
103+
$openApiContext['example'] = $openApiContext['default'];
104+
}
105+
106+
$openApiContext['type'] = $this->type;
107+
$openApiContext['nullable'] = $this->nullable;
108+
109+
110+
111+
return new ApiProperty(
112+
description: $this->description,
113+
readable: $this->readable,
114+
writable: $this->writeable,
115+
openapiContext: $openApiContext,
116+
types: $this->type,
117+
property: $this->property
118+
);
119+
}
67120
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
24+
namespace App\ApiPlatform\DocumentedAPIProperties;
25+
26+
use ApiPlatform\Metadata\ApiProperty;
27+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
28+
use ReflectionClass;
29+
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
30+
31+
/**
32+
* This decorator adds the virtual properties defined by the DocumentedAPIProperty attribute to the property metadata
33+
* which then get picked up by the openapi schema generator
34+
*/
35+
#[AsDecorator('api_platform.metadata.property.metadata_factory')]
36+
class PropertyMetadataFactory implements PropertyMetadataFactoryInterface
37+
{
38+
public function __construct(private PropertyMetadataFactoryInterface $decorated)
39+
{
40+
}
41+
42+
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
43+
{
44+
$metadata = $this->decorated->create($resourceClass, $property, $options);
45+
46+
//Only become active in the context of the openapi schema generation
47+
if (!isset($options['schema_type'])) {
48+
return $metadata;
49+
}
50+
51+
if (!class_exists($resourceClass)) {
52+
return $metadata;
53+
}
54+
55+
$refClass = new ReflectionClass($resourceClass);
56+
$attributes = $refClass->getAttributes(DocumentedAPIProperty::class);
57+
58+
//Look for the DocumentedAPIProperty attribute with the given property name
59+
foreach ($attributes as $attribute) {
60+
/** @var DocumentedAPIProperty $api_property */
61+
$api_property = $attribute->newInstance();
62+
//If attribute not matches the property name, skip it
63+
if ($api_property->property !== $property) {
64+
continue;
65+
}
66+
67+
//Return the virtual property
68+
return $api_property->toAPIProperty();
69+
}
70+
71+
return $metadata;
72+
}
73+
}

0 commit comments

Comments
 (0)