From e9a5db78462f6e95139984ff27588756a7e996fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20=C4=8Cupi=C4=87?= Date: Thu, 5 Sep 2024 19:12:45 +0200 Subject: [PATCH 01/12] Require new Cloudinary PHP API v2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4f8fc547..72d117e6 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ ], "require": { "php": "^8.1", - "cloudinary/cloudinary_php": "^1.20", + "cloudinary/cloudinary_php": "^2.0", "symfony/mime": "^5.4 || ^6.2", "symfony/twig-bundle": "^5.4 || ^6.2", "symfony/framework-bundle": "^5.4 || ^6.2", From d662a2429c7d21150a2ef0891bfe5ca5f2a8e7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20=C4=8Cupi=C4=87?= Date: Thu, 5 Sep 2024 19:13:13 +0200 Subject: [PATCH 02/12] Fix upgrade docs --- docs/UPGRADE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index 2f22fc8c..213b485a 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -10,7 +10,7 @@ Also, there were a lot of changes during the decoupling from eZ to make things c * This bundle doesn't depend on eZ anymore and does not contain any eZ related integration * This bundle doesn't depend on Netgen Open Graph bundle anymore and does not contain any related integration -* Minimum supported version of PHP is now PHP 7.4 +* Minimum supported version of PHP is now PHP 8.1 * Most of the classes in the codebase are now `final` - if you have any overrides in your project, refactor them to use the Decorator pattern instead * The main value object `Netgen\Bundle\RemoteMediaBundle\Core\FieldType\RemoteMedia\Value` which was extending eZ field type value has now been renamed to `Netgen\RemoteMedia\API\Values\RemoteResource` and all methods use or return this new one; methods and properties remained the same; now it's a standalone value object * The variation object `Netgen\Bundle\RemoteMediaBundle\Core\FieldType\RemoteMedia\Variation` has been renamed to `Netgen\RemoteMedia\API\Values\Variation` and all methods use or return this new one; methods and properties remained the same @@ -46,7 +46,7 @@ Old object `Netgen\Bundle\RemoteMediaBundle\Core\FieldType\RemoteMedia` has been * all properties are now private and there are corresponding getters as well as some helper methods to manipulate with the object * static constructors have been removed from the value object -> they've been replaced with factory * now it contains `id` which represents the ID of the stored resource in the database (via Doctrine) -> can be `null` if the resource has been fetched from remote -* now it contains `remoteId` which is used to uniquely identify the resource on the cloud -> in case when cloud providers require multiple parameters to uniquely identify the resource (eg. Cloudinary requires `resourceId`, `resourceType` and `type`), each provider is responsible to merge this info into single array which will be used as `remoteId` +* now it contains `remoteId` which is used to uniquely identify the resource on the cloud -> in case when cloud providers require multiple parameters to uniquely identify the resource (eg. Cloudinary requires `resourceId`, `resourceType` and `type`), each provider is responsible to merge this info into single string which will be used as `remoteId` * `mediaType` property has been removed; now we have a single `type` and it's up to the provider to convert this to the appropriate type for cloud provider * now it contains two new types: `audio` and `document` * now it contains only single `url` property instead of both URL and secure URL - it's up to provider to decide whether it will provide secure or not secure URLs From d88c95cc3dad68362a473f806fc90401ad5597db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20=C4=8Cupi=C4=87?= Date: Thu, 5 Sep 2024 19:19:09 +0200 Subject: [PATCH 03/12] Refactor code to use new Cloudinary PHP API v2 --- .../Cloudinary/CloudinaryProvider.php | 9 +- .../Cloudinary/Factory/CloudinaryInstance.php | 5 +- .../Gateway/CloudinaryApiGateway.php | 105 +++++++++--------- .../Gateway/Log/MonologLoggedGateway.php | 6 +- .../Gateway/Log/MonologLoggedGatewayTest.php | 6 +- 5 files changed, 68 insertions(+), 63 deletions(-) diff --git a/lib/Core/Provider/Cloudinary/CloudinaryProvider.php b/lib/Core/Provider/Cloudinary/CloudinaryProvider.php index 4fd3d625..544f8d82 100644 --- a/lib/Core/Provider/Cloudinary/CloudinaryProvider.php +++ b/lib/Core/Provider/Cloudinary/CloudinaryProvider.php @@ -4,6 +4,7 @@ namespace Netgen\RemoteMedia\Core\Provider\Cloudinary; +use Cloudinary\Configuration\TagConfig; use Doctrine\ORM\EntityManagerInterface; use Netgen\RemoteMedia\API\Factory\DateTime as DateTimeFactoryInterface; use Netgen\RemoteMedia\API\Search\Query; @@ -28,7 +29,6 @@ use function array_merge; use function basename; use function count; -use function default_poster_options; use function preg_match; use function sprintf; use function str_replace; @@ -280,7 +280,12 @@ protected function internalBuildVideoThumbnail(RemoteResource $resource, array $ $resource instanceof AuthenticatedRemoteResource ? $resource->getToken() : null, ); - return new RemoteResourceVariation($resource, $thumbnailUrl, array_merge(default_poster_options(), $options)); + $defaultPosterOptions = [ + 'format' => TagConfig::VIDEO_POSTER_FORMAT, + 'resource_type' => 'video', + ]; + + return new RemoteResourceVariation($resource, $thumbnailUrl, array_merge($defaultPosterOptions, $options)); } protected function generatePictureTag(RemoteResource $resource, array $transformations = [], array $htmlAttributes = []): string diff --git a/lib/Core/Provider/Cloudinary/Factory/CloudinaryInstance.php b/lib/Core/Provider/Cloudinary/Factory/CloudinaryInstance.php index e44c41b2..7662ada0 100644 --- a/lib/Core/Provider/Cloudinary/Factory/CloudinaryInstance.php +++ b/lib/Core/Provider/Cloudinary/Factory/CloudinaryInstance.php @@ -4,7 +4,7 @@ namespace Netgen\RemoteMedia\Core\Provider\Cloudinary\Factory; -use Cloudinary; +use Cloudinary\Cloudinary; final class CloudinaryInstance { @@ -21,8 +21,7 @@ public function __construct( public function create(): Cloudinary { if (!$this->cloudinary instanceof Cloudinary) { - $this->cloudinary = new Cloudinary(); - $this->cloudinary->config( + $this->cloudinary = new Cloudinary( [ 'cloud_name' => $this->cloudName, 'api_key' => $this->apiKey, diff --git a/lib/Core/Provider/Cloudinary/Gateway/CloudinaryApiGateway.php b/lib/Core/Provider/Cloudinary/Gateway/CloudinaryApiGateway.php index 18ca950f..781a9cc2 100644 --- a/lib/Core/Provider/Cloudinary/Gateway/CloudinaryApiGateway.php +++ b/lib/Core/Provider/Cloudinary/Gateway/CloudinaryApiGateway.php @@ -4,10 +4,15 @@ namespace Netgen\RemoteMedia\Core\Provider\Cloudinary\Gateway; -use Cloudinary; -use Cloudinary\Api as CloudinaryApi; -use Cloudinary\Search as CloudinarySearch; -use Cloudinary\Uploader as CloudinaryUploader; +use Cloudinary\Asset\Image; +use Cloudinary\Cloudinary; +use Cloudinary\Api\Admin\AdminApi; +use Cloudinary\Api\Search\SearchApi; +use Cloudinary\Api\Upload\UploadApi; +use Cloudinary\Asset\Media; +use Cloudinary\Tag\ImageTag; +use Cloudinary\Tag\VideoTag; +use Cloudinary\Api\Exception\NotFound as CloudinaryNotFound; use Netgen\RemoteMedia\API\Factory\RemoteResource as RemoteResourceFactoryInterface; use Netgen\RemoteMedia\API\Factory\SearchResult as SearchResultFactoryInterface; use Netgen\RemoteMedia\API\Search\Query; @@ -26,10 +31,6 @@ use function array_map; use function array_merge; -use function cl_image_tag; -use function cl_video_tag; -use function cl_video_thumbnail_path; -use function cloudinary_url_internal; use function count; use function date; use function floor; @@ -41,11 +42,11 @@ final class CloudinaryApiGateway implements GatewayInterface { - private CloudinaryApi $cloudinaryApi; + private AdminApi $adminApi; - private CloudinaryUploader $cloudinaryUploader; + private UploadApi $uploadApi; - private CloudinarySearch $cloudinarySearch; + private SearchApi $searchApi; public function __construct( private Cloudinary $cloudinary, @@ -54,26 +55,26 @@ public function __construct( private SearchExpressionResolver $searchExpressionResolver, private AuthTokenResolver $authTokenResolver ) { - $this->cloudinaryUploader = new CloudinaryUploader(); - $this->cloudinaryApi = new CloudinaryApi(); - $this->cloudinarySearch = new CloudinarySearch(); + $this->adminApi = new AdminApi(); + $this->uploadApi = new UploadApi(); + $this->searchApi = new SearchApi(); } public function setServices( Cloudinary $cloudinary, - CloudinaryUploader $cloudinaryUploader, - CloudinaryApi $cloudinaryApi, - CloudinarySearch $cloudinarySearch + UploadApi $uploadApi, + AdminApi $adminApi, + SearchApi $searchApi, ): void { $this->cloudinary = $cloudinary; - $this->cloudinaryUploader = $cloudinaryUploader; - $this->cloudinaryApi = $cloudinaryApi; - $this->cloudinarySearch = $cloudinarySearch; + $this->uploadApi = $uploadApi; + $this->adminApi = $adminApi; + $this->searchApi = $searchApi; } public function usage(): StatusData { - $usage = $this->cloudinaryApi->usage(); + $usage = $this->adminApi->usage(); return new StatusData([ 'plan' => $usage['plan'], @@ -102,7 +103,7 @@ public function isEncryptionEnabled(): bool public function countResources(): int { - $usage = $this->cloudinaryApi->usage(); + $usage = $this->adminApi->usage(); return (int) $usage['resources']; } @@ -111,9 +112,9 @@ public function countResourcesInFolder(string $folder): int { $expression = sprintf('folder:%s/*', $folder); - $search = $this->cloudinarySearch + $search = $this->searchApi ->expression($expression) - ->max_results(0); + ->maxResults(0); $response = $search->execute(); @@ -124,8 +125,8 @@ public function listFolders(): array { return array_map( static fn ($value) => $value['path'], - $this->cloudinaryApi - ->root_folders() + $this->adminApi + ->rootFolders() ->getArrayCopy()['folders'], ); } @@ -135,24 +136,24 @@ public function listSubFolders(string $parentFolder): array try { return array_map( static fn ($value) => $value['path'], - $this->cloudinaryApi - ->subfolders($parentFolder) + $this->adminApi + ->subFolders($parentFolder) ->getArrayCopy()['folders'], ); - } catch (CloudinaryApi\NotFound $e) { + } catch (CloudinaryNotFound $e) { throw new FolderNotFoundException(Folder::fromPath($parentFolder)); } } public function createFolder(string $path): void { - $this->cloudinaryApi->create_folder($path); + $this->adminApi->createFolder($path); } public function get(CloudinaryRemoteId $remoteId): RemoteResource { try { - $response = $this->cloudinaryApi->resource( + $response = $this->adminApi->asset( $remoteId->getResourceId(), [ 'type' => $remoteId->getType(), @@ -164,14 +165,14 @@ public function get(CloudinaryRemoteId $remoteId): RemoteResource ); return $this->remoteResourceFactory->create((array) $response); - } catch (CloudinaryApi\NotFound $e) { + } catch (CloudinaryNotFound $e) { throw new RemoteResourceNotFoundException($remoteId->getRemoteId()); } } public function upload(string $fileUri, array $options): RemoteResource { - $response = $this->cloudinaryUploader->upload($fileUri, $options); + $response = $this->uploadApi->upload($fileUri, $options); $resource = $this->remoteResourceFactory->create((array) $response); if ($response['existing'] ?? false) { @@ -187,8 +188,8 @@ public function update(CloudinaryRemoteId $remoteId, array $options): void $options['resource_type'] = $remoteId->getResourceType(); try { - $this->cloudinaryUploader->explicit($remoteId->getResourceId(), $options); - } catch (CloudinaryApi\NotFound $e) { + $this->uploadApi->explicit($remoteId->getResourceId(), $options); + } catch (CloudinaryNotFound $e) { throw new RemoteResourceNotFoundException($remoteId->getRemoteId()); } } @@ -201,8 +202,8 @@ public function removeAllTagsFromResource(CloudinaryRemoteId $remoteId): void ]; try { - $this->cloudinaryUploader->remove_all_tags([$remoteId->getResourceId()], $options); - } catch (CloudinaryApi\NotFound $e) { + $this->uploadApi->removeAllTags([$remoteId->getResourceId()], $options); + } catch (CloudinaryNotFound $e) { throw new RemoteResourceNotFoundException($remoteId->getRemoteId()); } } @@ -215,7 +216,7 @@ public function delete(CloudinaryRemoteId $remoteId): void 'resource_type' => $remoteId->getResourceType(), ]; - $this->cloudinaryUploader->destroy($remoteId->getResourceId(), $options); + $this->uploadApi->destroy($remoteId->getResourceId(), $options); } public function getAuthenticatedUrl(CloudinaryRemoteId $remoteId, AuthToken $token): string @@ -229,7 +230,7 @@ public function getAuthenticatedUrl(CloudinaryRemoteId $remoteId, AuthToken $tok $this->authTokenResolver->resolve($token), ); - return cloudinary_url_internal($remoteId->getResourceId(), $options); + return Media::fromParams($remoteId->getResourceId(), $options)->toUrl(); } public function getVariationUrl(CloudinaryRemoteId $remoteId, array $transformations, ?AuthToken $token = null): string @@ -248,19 +249,19 @@ public function getVariationUrl(CloudinaryRemoteId $remoteId, array $transformat ); } - return cloudinary_url_internal($remoteId->getResourceId(), $options); + return Media::fromParams($remoteId->getResourceId(), $options)->toUrl(); } public function search(Query $query): Result { - $search = $this->cloudinarySearch + $search = $this->searchApi ->expression($this->searchExpressionResolver->resolve($query)) - ->max_results($query->getLimit()) - ->with_field('context') - ->with_field('tags'); + ->maxResults($query->getLimit()) + ->withField('context') + ->withField('tags'); if ($query->getNextCursor() !== null) { - $search->next_cursor($query->getNextCursor()); + $search->nextCursor($query->getNextCursor()); } $response = $search->execute(); @@ -270,9 +271,9 @@ public function search(Query $query): Result public function searchCount(Query $query): int { - $search = $this->cloudinarySearch + $search = $this->searchApi ->expression($this->searchExpressionResolver->resolve($query)) - ->max_results(0); + ->maxResults(0); $response = $search->execute(); @@ -287,7 +288,7 @@ public function listTags(): array $tags = []; do { - $result = $this->cloudinaryApi->tags($options); + $result = $this->adminApi->tags($options); $tags = array_merge($tags, $result['tags']); $nextCursor = $result['next_cursor'] ?? null; @@ -312,7 +313,7 @@ public function getVideoThumbnail(CloudinaryRemoteId $remoteId, array $options = ); } - return cl_video_thumbnail_path($remoteId->getResourceId(), $options); + return Image::fromParams($remoteId->getResourceId(), $options)->toUrl(); } public function getImageTag(CloudinaryRemoteId $remoteId, array $options = [], ?AuthToken $token = null): string @@ -328,7 +329,7 @@ public function getImageTag(CloudinaryRemoteId $remoteId, array $options = [], ? ); } - return cl_image_tag($remoteId->getResourceId(), $options); + return ImageTag::fromParams($remoteId->getResourceId(), $options)->toTag(); } public function getVideoTag(CloudinaryRemoteId $remoteId, array $options = [], ?AuthToken $token = null): string @@ -344,7 +345,7 @@ public function getVideoTag(CloudinaryRemoteId $remoteId, array $options = [], ? ); } - return cl_video_tag($remoteId->getResourceId(), $options); + return VideoTag::fromParams($remoteId->getResourceId(), $options)->toTag(); } public function getDownloadLink(CloudinaryRemoteId $remoteId, array $options = [], ?AuthToken $token = null): string @@ -360,7 +361,7 @@ public function getDownloadLink(CloudinaryRemoteId $remoteId, array $options = [ ); } - return $this->cloudinary->cloudinary_url($remoteId->getResourceId(), $options); + return Media::fromParams($remoteId->getResourceId(), $options)->toUrl(); } private function formatBytes(int $bytes, int $precision = 2): string diff --git a/lib/Core/Provider/Cloudinary/Gateway/Log/MonologLoggedGateway.php b/lib/Core/Provider/Cloudinary/Gateway/Log/MonologLoggedGateway.php index 4caa85f1..eaefc63f 100644 --- a/lib/Core/Provider/Cloudinary/Gateway/Log/MonologLoggedGateway.php +++ b/lib/Core/Provider/Cloudinary/Gateway/Log/MonologLoggedGateway.php @@ -144,21 +144,21 @@ public function listTags(): array public function getVideoThumbnail(CloudinaryRemoteId $remoteId, array $options = [], ?AuthToken $token = null): string { - $this->logger->info("[INTERNAL][FREE] getVideoThumbnail(\"{$remoteId->getRemoteId()}\") -> cl_video_thumbnail_path(\"{$remoteId->getRemoteId()}\")"); + $this->logger->info("[INTERNAL][FREE] getVideoThumbnail(\"{$remoteId->getRemoteId()}\") -> Image::fromParams(\"{$remoteId->getRemoteId()}\")"); return $this->gateway->getVideoThumbnail($remoteId, $options, $token); } public function getImageTag(CloudinaryRemoteId $remoteId, array $options = [], ?AuthToken $token = null): string { - $this->logger->info("[INTERNAL][FREE] getImageTag(\"{$remoteId->getRemoteId()}\") -> cl_image_tag(\"{$remoteId->getRemoteId()}\")"); + $this->logger->info("[INTERNAL][FREE] getImageTag(\"{$remoteId->getRemoteId()}\") -> ImageTag::fromParams(\"{$remoteId->getRemoteId()}\")"); return $this->gateway->getImageTag($remoteId, $options, $token); } public function getVideoTag(CloudinaryRemoteId $remoteId, array $options = [], ?AuthToken $token = null): string { - $this->logger->info("[INTERNAL][FREE] getVideoTag(\"{$remoteId->getRemoteId()}\") -> cl_video_tag(\"{$remoteId->getRemoteId()}\")"); + $this->logger->info("[INTERNAL][FREE] getVideoTag(\"{$remoteId->getRemoteId()}\") -> VideoTag::fromParams(\"{$remoteId->getRemoteId()}\")"); return $this->gateway->getVideoTag($remoteId, $options, $token); } diff --git a/tests/lib/Core/Provider/Cloudinary/Gateway/Log/MonologLoggedGatewayTest.php b/tests/lib/Core/Provider/Cloudinary/Gateway/Log/MonologLoggedGatewayTest.php index 4f4173ea..3928da1b 100644 --- a/tests/lib/Core/Provider/Cloudinary/Gateway/Log/MonologLoggedGatewayTest.php +++ b/tests/lib/Core/Provider/Cloudinary/Gateway/Log/MonologLoggedGatewayTest.php @@ -456,7 +456,7 @@ public function testGetVideoThumbnail(): void $this->loggerMock ->expects(self::once()) ->method('info') - ->with("[INTERNAL][FREE] getVideoThumbnail(\"{$remoteId->getRemoteId()}\") -> cl_video_thumbnail_path(\"{$remoteId->getRemoteId()}\")"); + ->with("[INTERNAL][FREE] getVideoThumbnail(\"{$remoteId->getRemoteId()}\") -> Image::fromParams(\"{$remoteId->getRemoteId()}\")"); self::assertSame( 'video_thumbnail.jpg', @@ -477,7 +477,7 @@ public function testGetImageTag(): void $this->loggerMock ->expects(self::once()) ->method('info') - ->with("[INTERNAL][FREE] getImageTag(\"{$remoteId->getRemoteId()}\") -> cl_image_tag(\"{$remoteId->getRemoteId()}\")"); + ->with("[INTERNAL][FREE] getImageTag(\"{$remoteId->getRemoteId()}\") -> ImageTag::fromParams(\"{$remoteId->getRemoteId()}\")"); self::assertSame( '', @@ -498,7 +498,7 @@ public function testGetVideoTag(): void $this->loggerMock ->expects(self::once()) ->method('info') - ->with("[INTERNAL][FREE] getVideoTag(\"{$remoteId->getRemoteId()}\") -> cl_video_tag(\"{$remoteId->getRemoteId()}\")"); + ->with("[INTERNAL][FREE] getVideoTag(\"{$remoteId->getRemoteId()}\") -> VideoTag::fromParams(\"{$remoteId->getRemoteId()}\")"); self::assertSame( '