├── assets └── php-management-api-client-github-repository.png ├── src ├── QueryParameters │ ├── Type │ │ ├── Direction.php │ │ └── SortBy.php │ ├── WorkflowStageChangesParams.php │ ├── Filters │ │ ├── Filter.php │ │ └── QueryFilters.php │ ├── PaginationParams.php │ ├── WorkflowStagesParams.php │ ├── ComponentsParams.php │ ├── AssetsParams.php │ └── StoriesParams.php ├── Exceptions │ ├── StoryblokApiException.php │ ├── InvalidStoryDataException.php │ ├── StoryblokFormatException.php │ └── InvalidComponentDataException.php ├── Data │ ├── StoryblokDataInterface.php │ ├── Spaces.php │ ├── Components.php │ ├── Stories.php │ ├── ComponentFolders.php │ ├── SpaceEnvironments.php │ ├── Assets.php │ ├── WorkflowsData.php │ ├── WorkflowStagesData.php │ ├── WorkflowStageChanges.php │ ├── Enum │ │ └── Region.php │ ├── Tags.php │ ├── StoryblokData.php │ ├── StoryBaseData.php │ ├── WorkflowStageData.php │ ├── Traits │ │ └── AssetMethods.php │ ├── WorkflowData.php │ ├── Tag.php │ ├── Fields │ │ └── AssetField.php │ ├── WorkflowStageChange.php │ ├── SpaceEnvironment.php │ ├── IterableDataTrait.php │ ├── User.php │ ├── ComponentFolder.php │ ├── Asset.php │ ├── StoryCollectionItem.php │ ├── StoryComponent.php │ ├── Space.php │ ├── Story.php │ ├── Component.php │ └── BaseData.php ├── Response │ ├── TagsResponse.php │ ├── SpacesResponse.php │ ├── WorkflowStageChangesResponse.php │ ├── AssetsResponse.php │ ├── StoriesResponse.php │ ├── StoryblokResponseInterface.php │ ├── UserResponse.php │ ├── SpaceResponse.php │ ├── TagResponse.php │ ├── ComponentsResponse.php │ ├── StoryResponse.php │ ├── AssetUploadResponse.php │ ├── ComponentResponse.php │ ├── AssetResponse.php │ ├── WorkflowStageChangeResponse.php │ └── StoryblokResponse.php ├── Endpoints │ ├── UserApi.php │ ├── EndpointSpace.php │ ├── EndpointBase.php │ ├── ManagementApi.php │ ├── TagApi.php │ ├── WorkflowApi.php │ ├── WorkflowStageApi.php │ ├── WorkflowStageChangeApi.php │ ├── SpaceApi.php │ ├── ComponentApi.php │ ├── AssetApi.php │ ├── StoryBulkApi.php │ └── StoryApi.php ├── StoryblokUtils.php └── ManagementApiClient.php ├── rector.php ├── composer.json └── license-checker.php /assets/php-management-api-client-github-repository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storyblok/php-management-api-client/HEAD/assets/php-management-api-client-github-repository.png -------------------------------------------------------------------------------- /src/QueryParameters/Type/Direction.php: -------------------------------------------------------------------------------- 1 | field, $this->direction->value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Data/Spaces.php: -------------------------------------------------------------------------------- 1 | count(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryParameters/WorkflowStageChangesParams.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(): array 18 | { 19 | return ['with_story' => $this->withStory]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Data/Components.php: -------------------------------------------------------------------------------- 1 | count(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Data/Stories.php: -------------------------------------------------------------------------------- 1 | count(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Data/ComponentFolders.php: -------------------------------------------------------------------------------- 1 | count(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Data/SpaceEnvironments.php: -------------------------------------------------------------------------------- 1 | count(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Response/TagsResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 17 | if (array_key_exists($key, $array)) { 18 | return new Tags($array[$key]); 19 | } 20 | 21 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Data/Assets.php: -------------------------------------------------------------------------------- 1 | > $data 25 | */ 26 | public static function makeFromResponse(array $data): self 27 | { 28 | return new self($data["assets"] ?? []); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([__DIR__ . "/src", __DIR__ . "/tests"]) 9 | ->withPhpSets(php83: true) 10 | ->withPreparedSets( 11 | deadCode: true, 12 | codeQuality: true, 13 | codingStyle: true, 14 | typeDeclarations: true, 15 | // naming: true, 16 | instanceOf: true, 17 | 18 | earlyReturn: true, 19 | carbon: true, 20 | rectorPreset: true, 21 | phpunitCodeQuality: true, 22 | //privatization: true, 23 | ); 24 | //->withTypeCoverageLevel(100) 25 | //->withDeadCodeLevel(100) 26 | //->withCodeQualityLevel(100); 27 | -------------------------------------------------------------------------------- /src/QueryParameters/Filters/Filter.php: -------------------------------------------------------------------------------- 1 | |string $value 11 | */ 12 | public function __construct( 13 | public readonly string $field, 14 | public readonly string $operator, 15 | public readonly array|string $value, 16 | ) {} 17 | 18 | /** 19 | * @return mixed[] 20 | */ 21 | public function toArray(): array 22 | { 23 | 24 | return [$this->field => [ 25 | $this->operator => is_array($this->value) ? implode(',', $this->value) : $this->value, 26 | ]]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Endpoints/UserApi.php: -------------------------------------------------------------------------------- 1 | makeHttpRequest( 20 | "GET", 21 | '/v1/users/me' 22 | ); 23 | return new UserResponse($httpResponse); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Endpoints/EndpointSpace.php: -------------------------------------------------------------------------------- 1 | > $data 23 | */ 24 | public static function makeFromResponse(array $data = []): self 25 | { 26 | return new self($data["workflows"] ?? []); 27 | } 28 | 29 | public function howManyWorkflows(): int 30 | { 31 | return $this->count(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Data/WorkflowStagesData.php: -------------------------------------------------------------------------------- 1 | > $data 23 | */ 24 | public static function makeFromResponse(array $data = []): self 25 | { 26 | return new self($data["workflow_stages"] ?? []); 27 | } 28 | 29 | public function howManyWorkflowStages(): int 30 | { 31 | return $this->count(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/QueryParameters/PaginationParams.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(): array 18 | { 19 | return ['page' => $this->page, 'per_page' => $this->perPage]; 20 | } 21 | 22 | public function page(): int 23 | { 24 | return $this->page; 25 | } 26 | 27 | public function perPage(): int 28 | { 29 | return $this->perPage; 30 | } 31 | 32 | public function incrementPage(): void 33 | { 34 | ++$this->page; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Data/WorkflowStageChanges.php: -------------------------------------------------------------------------------- 1 | > $data 23 | */ 24 | public static function makeFromResponse(array $data = []): self 25 | { 26 | return new self($data["workflow_stage_changes"] ?? []); 27 | } 28 | 29 | public function howManyWorkflowStageChanges(): int 30 | { 31 | return $this->count(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Response/SpacesResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 20 | if (array_key_exists($key, $array)) { 21 | return new Spaces($array[$key]); 22 | } 23 | 24 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Response/WorkflowStageChangesResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 19 | if (array_key_exists($key, $array)) { 20 | return new WorkflowStageChanges($array[$key]); 21 | } 22 | 23 | throw new StoryblokFormatException( 24 | sprintf("Expected '%s' in the response.", $key), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Response/AssetsResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 21 | if (array_key_exists($key, $array)) { 22 | return new Assets($array[$key]); 23 | } 24 | 25 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Response/StoriesResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 21 | if (array_key_exists($key, $array)) { 22 | return new Stories($array[$key]); 23 | } 24 | 25 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Data/Enum/Region.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public static function values(): array 25 | { 26 | return array_column(self::cases(), 'value'); 27 | } 28 | 29 | /** 30 | * Check if a value is a valid region. 31 | * 32 | * @param string $value the code of region to be validated 33 | * @return bool true if the region is one of the valid region (Upper case) 34 | */ 35 | public static function isValid(string $value): bool 36 | { 37 | return in_array($value, self::values(), true); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Data/Tags.php: -------------------------------------------------------------------------------- 1 | data as $tag) { 33 | if (is_array($tag) && array_key_exists("name", $tag)) { 34 | $tagsArray[] = $tag["name"]; 35 | } 36 | 37 | } 38 | 39 | return $tagsArray; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Data/StoryblokData.php: -------------------------------------------------------------------------------- 1 | $data The initial data to store in the object. 21 | */ 22 | public function __construct(array $data = []) 23 | { 24 | $this->data = $data; 25 | } 26 | 27 | /** 28 | * Factory method to create a new instance of StoryblokData. 29 | * 30 | * @param array $data The data to initialize the object with. 31 | * @return StoryblokData A new instance of StoryblokData. 32 | */ 33 | public static function make(array $data = []): StoryblokData 34 | { 35 | return new StoryblokData($data); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Data/StoryBaseData.php: -------------------------------------------------------------------------------- 1 | getString('slug'); 14 | } 15 | 16 | public function hasWorkflowStage(): bool 17 | { 18 | $workflowStageId = $this->getInt('stage.workflow_stage_id', 0); 19 | return $workflowStageId > 0; 20 | } 21 | 22 | public function hasTags(): bool 23 | { 24 | $tags = $this->getArray('tag_list', []); 25 | return $tags !== []; 26 | } 27 | 28 | public function tagListAsString(): string 29 | { 30 | $tags = $this->getArray('tag_list', []); 31 | return implode(", ", $tags); 32 | } 33 | 34 | /** 35 | * Returns the list of tags as array. 36 | * 37 | * @return array 38 | */ 39 | public function tagListAsArray(): array 40 | { 41 | return $this->getArray('tag_list', []); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Data/WorkflowStageData.php: -------------------------------------------------------------------------------- 1 | > $data 14 | */ 15 | public static function makeFromResponse(array $data = []): self 16 | { 17 | return new self($data["workflow_stage"] ?? []); 18 | } 19 | 20 | #[\Override] 21 | public static function make(array $data = []): self 22 | { 23 | return new self($data); 24 | } 25 | 26 | public function setName(string $name): void 27 | { 28 | $this->set('name', $name); 29 | } 30 | 31 | public function setWorkflowId(string|int $workflowId): void 32 | { 33 | $this->set('workflow_id', $workflowId); 34 | } 35 | 36 | public function name(): string 37 | { 38 | return $this->getString('name', ""); 39 | } 40 | 41 | public function id(): string 42 | { 43 | return $this->getString('id', ""); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Response/StoryblokResponseInterface.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function toArray(): array; 36 | 37 | public function data(): StoryblokDataInterface; 38 | 39 | public function getLastCalledUrl(): string; 40 | 41 | public function isOk(): bool; 42 | } 43 | -------------------------------------------------------------------------------- /src/QueryParameters/Filters/QueryFilters.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $array = []; 13 | 14 | public function add(Filter $filter): QueryFilters 15 | { 16 | $this->array[] = $filter; 17 | return $this; 18 | } 19 | 20 | /** 21 | * @return mixed[] 22 | */ 23 | public function toArray(): array 24 | { 25 | /** @var mixed[] $array */ 26 | $array = []; 27 | 28 | foreach ($this->array as $filter) { 29 | if (! array_key_exists("filter_query", $array)) { 30 | $array["filter_query"] = []; 31 | } 32 | 33 | if (! array_key_exists($filter->field, $array["filter_query"])) { 34 | $array["filter_query"][$filter->field] = []; 35 | } 36 | 37 | $array["filter_query"][$filter->field][$filter->operator] = is_array($filter->value) ? implode(',', $filter->value) : $filter->value; 38 | 39 | } 40 | 41 | return $array; 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Data/Traits/AssetMethods.php: -------------------------------------------------------------------------------- 1 | getString("id"); 12 | } 13 | 14 | public function filename(): string 15 | { 16 | return $this->getString("filename", ""); 17 | } 18 | 19 | public function alt(): string 20 | { 21 | return $this->getString("alt", ""); 22 | } 23 | 24 | public function name(): string 25 | { 26 | return $this->getString("name", ""); 27 | } 28 | 29 | public function focus(): string 30 | { 31 | return $this->getString("focus", ""); 32 | } 33 | 34 | public function title(): string 35 | { 36 | return $this->getString("title", ""); 37 | } 38 | 39 | public function source(): string 40 | { 41 | return $this->getString("source", ""); 42 | } 43 | 44 | public function fieldtype(): string 45 | { 46 | return $this->getString("copyright", ""); 47 | } 48 | 49 | public function copyright(): string 50 | { 51 | return $this->getString("copyright", ""); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Data/WorkflowData.php: -------------------------------------------------------------------------------- 1 | > $data 14 | */ 15 | public static function makeFromResponse(array $data = []): self 16 | { 17 | return new self($data["workflow"] ?? []); 18 | } 19 | 20 | #[\Override] 21 | public static function make(array $data = []): self 22 | { 23 | return new self($data); 24 | } 25 | 26 | public function setName(string $name): void 27 | { 28 | $this->set('name', $name); 29 | } 30 | 31 | public function name(): string 32 | { 33 | return $this->getString('name', ""); 34 | } 35 | 36 | public function id(): string 37 | { 38 | return $this->getString('id', ""); 39 | } 40 | 41 | public function isDefault(): bool 42 | { 43 | return $this->getBoolean('is_default', false); 44 | } 45 | 46 | /** 47 | * @return mixed[]|null 48 | */ 49 | public function contentTypes(): null|array 50 | { 51 | return $this->getArray('content_types', []); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Response/UserResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 30 | if (array_key_exists($key, $array)) { 31 | return new User($array[$key]); 32 | } 33 | 34 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Response/SpaceResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 30 | if (array_key_exists($key, $array)) { 31 | return Space::make($array[$key]); 32 | } 33 | 34 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Response/TagResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 31 | if (array_key_exists($key, $array)) { 32 | return Tag::make($array[$key]); 33 | } 34 | 35 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Response/ComponentsResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 23 | if (array_key_exists($key, $array)) { 24 | return new Components($array[$key]); 25 | } 26 | 27 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 28 | } 29 | 30 | public function dataFolders(): ComponentFolders 31 | { 32 | $key = "component_groups"; 33 | $array = $this->toArray(); 34 | if (array_key_exists($key, $array)) { 35 | return new ComponentFolders($array[$key]); 36 | } 37 | 38 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Response/StoryResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 31 | if (array_key_exists($key, $array)) { 32 | return Story::make($array[$key]); 33 | } 34 | 35 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Response/AssetUploadResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 32 | //if (array_key_exists($key, $array)) { 33 | return Asset::make($array); 34 | //} 35 | 36 | //throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/QueryParameters/WorkflowStagesParams.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function toArray(): array 25 | { 26 | $array = []; 27 | if (null !== $this->inWorkflowId) { 28 | $array['in_workflow'] = $this->inWorkflowId; 29 | } 30 | 31 | if (null !== $this->search) { 32 | $array['search'] = $this->search; 33 | } 34 | 35 | if (null !== $this->excludeId) { 36 | $array['exclude_id'] = $this->excludeId; 37 | } 38 | 39 | if (null !== $this->byIds) { 40 | if (is_array($this->byIds)) { 41 | $array['by_ids'] = implode(",", $this->byIds); 42 | } 43 | 44 | if (is_string($this->byIds)) { 45 | $array['by_ids'] = $this->byIds; 46 | } 47 | } 48 | 49 | return $array; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Response/ComponentResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 32 | if (array_key_exists($key, $array)) { 33 | return Component::make($array[$key]); 34 | } 35 | 36 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Response/AssetResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 32 | return Asset::make($array); 33 | /* 34 | if (array_key_exists($key, $array)) { 35 | return AssetData::make($array[$key]); 36 | } 37 | 38 | throw new StoryblokFormatException(sprintf("Expected '%s' in the response.", $key)); 39 | */ 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Data/Tag.php: -------------------------------------------------------------------------------- 1 | data = []; 15 | $this->data['name'] = $name; 16 | } 17 | 18 | /** 19 | * @param mixed[] $data 20 | * @throws StoryblokFormatException 21 | */ 22 | public static function make(array $data = []): self 23 | { 24 | $dataObject = new StoryblokData($data); 25 | if (!($dataObject->hasKey('name'))) { 26 | // is not valid 27 | } 28 | 29 | $tag = new Tag( 30 | $dataObject->getString("name") 31 | ); 32 | $tag->setData($dataObject->toArray()); 33 | // validate 34 | if (! $tag->isValid()) { 35 | throw new StoryblokFormatException("Tag is not valid"); 36 | } 37 | 38 | return $tag; 39 | 40 | } 41 | 42 | public function isValid(): bool 43 | { 44 | return $this->hasKey('name'); 45 | } 46 | 47 | public function name(): string 48 | { 49 | return $this->getString('name'); 50 | } 51 | 52 | public function id(): string 53 | { 54 | return $this->getString('id'); 55 | } 56 | 57 | public function taggingsCount(): int|null 58 | { 59 | return $this->getInt('taggings_count', 0); 60 | } 61 | 62 | public function tagOnStories(): int|null 63 | { 64 | return $this->getInt('tag_on_stories', 0); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storyblok/php-management-api-client", 3 | "description": "Storyblok PHP Client for Management API", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Storyblok\\ManagementApi\\": "src/" 9 | } 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Roberto Butti", 14 | "email": "roberto.butti@gmail.com" 15 | } 16 | ], 17 | "minimum-stability": "stable", 18 | "require": { 19 | "php": ">=8.3", 20 | "symfony/http-client": "^7.0" 21 | }, 22 | "require-dev": { 23 | "phpstan/phpstan": "^2.0", 24 | "rector/rector": "^2", 25 | "friendsofphp/php-cs-fixer": "^3.65", 26 | "pestphp/pest": "^3.7" 27 | }, 28 | "scripts": { 29 | "license-check": "php license-checker.php", 30 | "static-code": "vendor/bin/phpstan analyse", 31 | "style-fix-code": "vendor/bin/php-cs-fixer fix", 32 | "style-check-code": "vendor/bin/php-cs-fixer check", 33 | "test-code": "vendor/bin/pest", 34 | "test-code-ci": "vendor/bin/pest -c . --ci --cache-directory ./tmp", 35 | "refactor-check-code": "vendor/bin/rector --dry-run", 36 | "test-coverage": "vendor/bin/pest --configuration=phpunit.xml.dist --coverage-html .build/html", 37 | "all-checks": [ 38 | "@style-check-code", 39 | "@static-code", 40 | "@refactor-check-code", 41 | "@test-code" 42 | ] 43 | }, 44 | "config": { 45 | "allow-plugins": { 46 | "pestphp/pest-plugin": true 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/QueryParameters/ComponentsParams.php: -------------------------------------------------------------------------------- 1 | |string|null $byIds 13 | */ 14 | public function __construct( 15 | private readonly array|string|null $byIds = null, 16 | private readonly ?SortBy $sortBy = null, 17 | private readonly bool|null $isRoot = null, 18 | private readonly string|null $search = null, 19 | private readonly string|null $inGroup = null, 20 | ) {} 21 | 22 | /** 23 | * @return array 24 | */ 25 | public function toArray(): array 26 | { 27 | $array = []; 28 | 29 | if (null !== $this->search) { 30 | $array['search'] = $this->search; 31 | } 32 | 33 | if ($this->sortBy instanceof SortBy) { 34 | $array['sort_by'] = $this->sortBy->toString(); 35 | } 36 | 37 | if ($this->isRoot === true) { 38 | $array['is_root'] = "1"; 39 | } 40 | 41 | if (null !== $this->byIds) { 42 | if (is_array($this->byIds)) { 43 | $array['by_ids'] = implode(",", $this->byIds); 44 | } 45 | 46 | if (is_string($this->byIds)) { 47 | $array['by_ids'] = $this->byIds; 48 | } 49 | } 50 | 51 | if (null !== $this->search) { 52 | $array['search'] = $this->search; 53 | } 54 | 55 | if (null !== $this->inGroup) { 56 | $array['in_group'] = $this->inGroup; 57 | } 58 | 59 | return $array; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Data/Fields/AssetField.php: -------------------------------------------------------------------------------- 1 | data = [ 18 | "id" => null, 19 | "alt" => "", 20 | "name" => "", 21 | "focus" => "", 22 | "title" => "", 23 | "source" => "", 24 | "filename" => $filename, 25 | "copyright" => "", 26 | "fieldtype" => "asset", 27 | "meta_data" => (object) [], 28 | ]; 29 | } 30 | 31 | public static function makeFromAsset(Asset $asset): self 32 | { 33 | $field = new self(); 34 | $attributes = [ 35 | "id", 36 | "alt", 37 | "name", 38 | "focus", 39 | "title", 40 | "source", 41 | "filename", 42 | "copyright", 43 | "fieldtype", 44 | "meta_data", 45 | ]; 46 | foreach ($attributes as $attribute) { 47 | $field->set($attribute, $asset->get($attribute)); 48 | } 49 | 50 | $field->set("name", $asset->getString("name")); 51 | $field->set("filename", $asset->filenameCDN()); 52 | $field->set("is_external_url", false); 53 | $field->set("meta_data", (object) $asset->getArray("meta_data")); 54 | return $field; 55 | } 56 | 57 | public function isExternalUrl(): bool 58 | { 59 | return $this->getBoolean("is_external_url", false); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Response/WorkflowStageChangeResponse.php: -------------------------------------------------------------------------------- 1 | toArray(); 33 | if (array_key_exists($key, $array)) { 34 | return WorkflowStageChange::make($array[$key]); 35 | } 36 | 37 | $additionalErrorString = ""; 38 | if (array_key_exists("message", $array)) { 39 | $additionalErrorString = " " . $array["message"]; 40 | } 41 | 42 | throw new StoryblokFormatException( 43 | sprintf( 44 | "Expected '%s' in the response.%s", 45 | $key, 46 | $additionalErrorString, 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Data/WorkflowStageChange.php: -------------------------------------------------------------------------------- 1 | > $data 13 | */ 14 | public static function makeFromResponse(array $data = []): self 15 | { 16 | return new self($data["workflow_stage_change"] ?? []); 17 | } 18 | 19 | #[\Override] 20 | public static function make(array $data = []): self 21 | { 22 | return new self($data); 23 | } 24 | 25 | public static function makeFromParams( 26 | int $storyId, 27 | int $workflowStageId, 28 | ?string $dueDate = null, 29 | ): self { 30 | $change = new self(); 31 | $change->setStoryAndStage($storyId, $workflowStageId); 32 | if (!is_null($dueDate)) { 33 | $change->setDueDate($dueDate); 34 | } 35 | 36 | return $change; 37 | } 38 | 39 | public function setStoryAndStage(int $storyId, int $workflowStageId): void 40 | { 41 | $this->setStoryId($storyId); 42 | $this->setWorkflowStageId($workflowStageId); 43 | } 44 | 45 | public function setStoryId(int $storyId): void 46 | { 47 | $this->set("story_id", $storyId); 48 | } 49 | 50 | public function setDueDate(string $dueDate): void 51 | { 52 | $this->set("due_date", $dueDate); 53 | } 54 | 55 | public function setWorkflowStageId(int $workflowStageId): void 56 | { 57 | $this->set("workflow_stage_id", $workflowStageId); 58 | } 59 | 60 | public function id(): ?int 61 | { 62 | return $this->getInt("id"); 63 | } 64 | 65 | public function workflowStageId(): ?int 66 | { 67 | return $this->getInt("workflow_stage_id"); 68 | } 69 | 70 | public function userId(): ?int 71 | { 72 | return $this->getInt("user_id"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Endpoints/EndpointBase.php: -------------------------------------------------------------------------------- 1 | httpClient = $managementClient->httpClient(); 29 | } 30 | 31 | /** 32 | * @param array $options 33 | * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface 34 | */ 35 | public function makeRequest( 36 | string $method = "GET", 37 | string $path = "/v1/spaces", 38 | array $options = [], 39 | string $dataClass = StoryblokData::class, 40 | ): StoryblokResponseInterface { 41 | $response = $this->makeHttpRequest( 42 | $method, 43 | $path, 44 | $options, 45 | ); 46 | return StoryblokResponse::make($response, $dataClass); 47 | } 48 | 49 | /** 50 | * @param array $options 51 | * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface 52 | */ 53 | public function makeHttpRequest( 54 | string $method, 55 | string $path, 56 | array $options = [], 57 | ): ResponseInterface { 58 | return $this->httpClient->request( 59 | $method, 60 | $path, 61 | $options, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Data/SpaceEnvironment.php: -------------------------------------------------------------------------------- 1 | data = []; 14 | $this->data["location"] = $location; 15 | $this->data["name"] = $name; 16 | } 17 | 18 | /** 19 | * @param mixed[] $data 20 | * @throws StoryblokFormatException 21 | */ 22 | public static function make(array $data = []): self 23 | { 24 | $dataObject = new StoryblokData($data); 25 | if (!$dataObject->hasKey("name")) { 26 | throw new StoryblokFormatException( 27 | "Environments/Preview URL is not valid, missing the name", 28 | ); 29 | } 30 | 31 | if (!$dataObject->hasKey("location")) { 32 | throw new StoryblokFormatException( 33 | "Environments/Preview URL is not valid, missing the location/URL", 34 | ); 35 | } 36 | 37 | return new self( 38 | $dataObject->getString("name"), 39 | $dataObject->getString("location"), 40 | ); 41 | } 42 | 43 | public function setName(string $name): void 44 | { 45 | $this->set("name", $name); 46 | } 47 | 48 | /** 49 | * Environment/Preview URL name 50 | */ 51 | public function name(): string 52 | { 53 | return $this->getString("name"); 54 | } 55 | 56 | public function setLocation(string $location): void 57 | { 58 | $this->set("location", $location); 59 | } 60 | 61 | /** 62 | * Environment/Preview URL location 63 | */ 64 | public function location(): string 65 | { 66 | return $this->getString("location"); 67 | } 68 | 69 | /** 70 | * Validates if the Environment/Preview URL data contains all required fields and valid values 71 | */ 72 | public function isValid(): bool 73 | { 74 | return $this->hasKey("name") && $this->hasKey("location"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Data/IterableDataTrait.php: -------------------------------------------------------------------------------- 1 | data); 14 | if (is_array($current)) { 15 | return ($this->getDataClass())::make($current); 16 | } 17 | 18 | return $current; 19 | } 20 | 21 | public function next(): void 22 | { 23 | next($this->data); 24 | } 25 | 26 | /** 27 | * Return the key of the current element 28 | * 29 | * @link https://php.net/manual/en/iterator.key.php 30 | * 31 | * @return string|int|null scalar on success, or null on failure. 32 | */ 33 | public function key(): string|int|null 34 | { 35 | return key($this->data); 36 | } 37 | 38 | public function valid(): bool 39 | { 40 | return !is_null($this->key()); 41 | } 42 | 43 | public function rewind(): void 44 | { 45 | reset($this->data); 46 | } 47 | 48 | public function offsetExists(mixed $offset): bool 49 | { 50 | return array_key_exists($offset, $this->data); 51 | } 52 | 53 | public function offsetGet(mixed $offset): mixed 54 | { 55 | return $this->get($offset); 56 | } 57 | 58 | public function offsetSet(mixed $offset, mixed $value): void 59 | { 60 | if (is_null($offset)) { 61 | $this->data[] = $value; 62 | } else { 63 | $this->data[$offset] = $value; 64 | } 65 | } 66 | 67 | public function offsetUnset(mixed $offset): void 68 | { 69 | unset($this->data[$offset]); 70 | } 71 | 72 | /** 73 | * It executes a provided function ($callback) once for each element. 74 | * @param callable $callback the function to call for each element 75 | */ 76 | public function forEach(callable $callback): self 77 | { 78 | $result = []; 79 | foreach ($this as $key => $item) { 80 | $result[$key] = $callback($item); 81 | } 82 | 83 | return new StoryblokData($result); 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Endpoints/ManagementApi.php: -------------------------------------------------------------------------------- 1 | $queryParams 14 | * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface 15 | */ 16 | public function get(string $path = "spaces", array $queryParams = []): StoryblokResponseInterface 17 | { 18 | return $this->makeRequest( 19 | "GET", 20 | '/v1/' . $path, 21 | [ 22 | "query" => $queryParams, 23 | ], 24 | ); 25 | } 26 | 27 | /** 28 | * @param array $payload 29 | * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface 30 | */ 31 | public function post(string $path, array $payload = []): StoryblokResponseInterface 32 | { 33 | return $this->makeRequest( 34 | "POST", 35 | "/v1/" . $path, 36 | [ 37 | "body" => $payload, 38 | ], 39 | ); 40 | } 41 | 42 | public function delete(string $path): StoryblokResponseInterface 43 | { 44 | return $this->makeRequest( 45 | "DELETE", 46 | '/v1/' . $path, 47 | ); 48 | } 49 | 50 | /** 51 | * Function for updating a resource. 52 | * Under the hood, is performed a PUT HTTP method 53 | * @param string $path the path of the API endpoint, 54 | * for example: spaces/1111/stories/22222 55 | * @param array $payload the Request Body Properties 56 | * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface 57 | */ 58 | public function put(string $path, array $payload = []): StoryblokResponseInterface 59 | { 60 | return $this->makeRequest( 61 | "PUT", 62 | "/v1/" . $path, 63 | [ 64 | "body" => $payload, 65 | ], 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /license-checker.php: -------------------------------------------------------------------------------- 1 | $info) { 41 | if (in_array($package, $excludedPackages, true)) { 42 | echo "⏩ Skipping excluded package: {$package}\n"; 43 | continue; 44 | } 45 | 46 | $checkedCount++; 47 | $packageLicenses = $info['license'] ?? []; 48 | $version = $info['version'] ?? 'unknown'; 49 | 50 | $hasAllowedLicense = false; 51 | foreach ($packageLicenses as $license) { 52 | if (in_array($license, $allowedLicenses, true)) { 53 | $hasAllowedLicense = true; 54 | break; 55 | } 56 | } 57 | 58 | if (!$hasAllowedLicense) { 59 | $violations[] = [ 60 | 'package' => $package, 61 | 'version' => $version, 62 | 'licenses' => $packageLicenses, 63 | ]; 64 | echo "❌ License violation: {$package} ({$version}) uses " . implode(', ', $packageLicenses) . "\n"; 65 | } else { 66 | echo "✅ {$package} ({$version}) uses " . implode(', ', $packageLicenses) . "\n"; 67 | } 68 | } 69 | 70 | echo "\n"; 71 | echo "Summary:\n"; 72 | echo "- Packages checked: {$checkedCount}\n"; 73 | echo "- Violations found: " . count($violations) . "\n"; 74 | 75 | if (count($violations) > 0) { 76 | echo "\nLicense violations detected. Please review the dependencies above.\n"; 77 | exit(1); 78 | } else { 79 | echo "\nAll dependencies comply with the allowed licenses.\n"; 80 | exit(0); 81 | } 82 | -------------------------------------------------------------------------------- /src/Data/User.php: -------------------------------------------------------------------------------- 1 | > $data 13 | */ 14 | public static function makeFromResponse(array $data = []): self 15 | { 16 | return new self($data["user"] ?? []); 17 | } 18 | 19 | #[\Override] 20 | public static function make(array $data = []): self 21 | { 22 | return new self($data); 23 | } 24 | 25 | public function orgName(): string 26 | { 27 | return $this->getString('org.name'); 28 | } 29 | 30 | public function username(): string 31 | { 32 | return $this->getString('username'); 33 | } 34 | 35 | public function firstname(): string 36 | { 37 | return $this->getString('firstname'); 38 | } 39 | 40 | public function lastname(): string 41 | { 42 | return $this->getString('lastname'); 43 | } 44 | 45 | public function id(): string 46 | { 47 | return $this->getString('id'); 48 | } 49 | 50 | public function orgRole(): string 51 | { 52 | return $this->getString('org_role'); 53 | } 54 | 55 | public function userId(): string 56 | { 57 | return $this->getString('userid'); 58 | } 59 | 60 | public function email(): string 61 | { 62 | return $this->getString('email'); 63 | } 64 | 65 | public function createdAt(string $format = 'Y-m-d H:i:s'): string|null 66 | { 67 | return $this->getFormattedDateTime( 68 | 'created_at', 69 | format: $format, 70 | ); 71 | } 72 | 73 | public function hasOrganization(): bool 74 | { 75 | return $this->getBoolean('has_org'); 76 | } 77 | 78 | public function hasPartner(): bool 79 | { 80 | return $this->getBoolean('has_partner'); 81 | } 82 | 83 | public function partnerStatus(): string 84 | { 85 | return $this->getString('partner_status'); 86 | } 87 | 88 | public function timezone(): string 89 | { 90 | return $this->getString('timezone'); 91 | } 92 | 93 | public function avatarUrl(?int $size = 72): string 94 | { 95 | $sizeString = ""; 96 | if (null !== $size) { 97 | $sizeString = $size . 'x' . $size . "/"; 98 | } 99 | 100 | return "https://img2.storyblok.com/" . $sizeString . $this->getString('avatar'); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/QueryParameters/AssetsParams.php: -------------------------------------------------------------------------------- 1 | |string|null $withTags Filter by specific tags 18 | */ 19 | public function __construct( 20 | private readonly int|string|null $inFolder = null, 21 | private readonly ?SortBy $sortBy = null, 22 | private readonly bool|null $isPrivate = null, 23 | private readonly string|null $search = null, 24 | private readonly string|null $byAlt = null, 25 | private readonly string|null $byCopyright = null, 26 | private readonly string|null $byTitle = null, 27 | private readonly array|string|null $withTags = null, 28 | ) {} 29 | 30 | /** 31 | * @return array 32 | */ 33 | public function toArray(): array 34 | { 35 | $array = []; 36 | if (null !== $this->inFolder) { 37 | $array['in_folder'] = $this->inFolder; 38 | } 39 | 40 | if ($this->sortBy instanceof \Storyblok\ManagementApi\QueryParameters\Type\SortBy) { 41 | $array['sort_by'] = $this->sortBy->toString(); 42 | } 43 | 44 | if (null !== $this->isPrivate && $this->isPrivate) { 45 | $array['is_private'] = "1"; 46 | } 47 | 48 | if (null !== $this->search) { 49 | $array['search'] = $this->search; 50 | } 51 | 52 | if (null !== $this->byAlt) { 53 | $array['by_alt'] = $this->byAlt; 54 | } 55 | 56 | if (null !== $this->byCopyright) { 57 | $array['by_copyright'] = $this->byCopyright; 58 | } 59 | 60 | if (null !== $this->byTitle) { 61 | $array['by_title'] = $this->byTitle; 62 | } 63 | 64 | if (null !== $this->withTags) { 65 | if (is_array($this->withTags)) { 66 | $array['with_tags'] = implode(",", $this->withTags); 67 | } 68 | 69 | if (is_string($this->withTags)) { 70 | $array['with_tags'] = $this->withTags; 71 | } 72 | 73 | } 74 | 75 | return $array; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Endpoints/TagApi.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'page' => $page, 20 | 'per_page' => $perPage, 21 | ], 22 | ]; 23 | $httpResponse = $this->makeHttpRequest( 24 | "GET", 25 | '/v1/spaces/' . $this->spaceId . '/tags', 26 | options: $options 27 | ); 28 | return new TagsResponse($httpResponse); 29 | } 30 | 31 | /** 32 | * @param string $name the tag name in string format 33 | * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface 34 | */ 35 | public function get(string $name): TagResponse 36 | { 37 | 38 | $httpResponse = $this->makeHttpRequest( 39 | "GET", 40 | '/v1/spaces/' . $this->spaceId . '/tags/' . $name 41 | ); 42 | 43 | return new TagResponse($httpResponse); 44 | } 45 | 46 | /** 47 | * @param $name 48 | */ 49 | public function delete(string $name): TagResponse 50 | { 51 | $httpResponse = $this->makeHttpRequest( 52 | "DELETE", 53 | '/v1/spaces/' . $this->spaceId . '/tags/' . $name 54 | ); 55 | return new TagResponse($httpResponse); 56 | } 57 | 58 | public function create(string $name): TagResponse 59 | { 60 | $httpResponse = $this->makeHttpRequest( 61 | "POST", 62 | "/v1/spaces/" . $this->spaceId . '/tags', 63 | [ 64 | "body" => [ 65 | "tag" => [ 66 | "name" => $name, 67 | ], 68 | ], 69 | ] 70 | ); 71 | return new TagResponse($httpResponse); 72 | } 73 | 74 | public function update(string $name, string $newName): TagResponse 75 | { 76 | $httpResponse = $this->makeHttpRequest( 77 | "POST", 78 | "/v1/spaces/" . $this->spaceId . '/tags/' . $name, 79 | [ 80 | "body" => [ 81 | "name" => $newName, 82 | ], 83 | ] 84 | ); 85 | return new TagResponse($httpResponse); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Endpoints/WorkflowApi.php: -------------------------------------------------------------------------------- 1 | [ 29 | 'content_type' => $contentType, 30 | ], 31 | ]; 32 | 33 | } 34 | 35 | return $this->makeRequest( 36 | "GET", 37 | '/v1/spaces/' . $this->spaceId . '/workflows', 38 | options: $options, 39 | dataClass: WorkflowsData::class, 40 | ); 41 | } 42 | 43 | public function get(string|int $workflowId): StoryblokResponseInterface 44 | { 45 | 46 | return $this->makeRequest( 47 | "GET", 48 | '/v1/spaces/' . $this->spaceId . '/workflows/' . $workflowId, 49 | dataClass: WorkflowData::class, 50 | ); 51 | } 52 | 53 | /** 54 | * @param string|int $workflowId the workflow identifier 55 | */ 56 | public function delete(string|int $workflowId): StoryblokResponseInterface 57 | { 58 | return $this->makeRequest( 59 | "DELETE", 60 | '/v1/spaces/' . $this->spaceId . '/workflows/' . $workflowId, 61 | dataClass: WorkflowData::class 62 | ); 63 | } 64 | 65 | public function create(StoryblokData $storyblokData): StoryblokResponseInterface 66 | { 67 | return $this->makeRequest( 68 | "POST", 69 | "/v1/spaces/" . $this->spaceId . '/workflows', 70 | [ 71 | "body" => [ 72 | "workflow" => $storyblokData->toArray(), 73 | ], 74 | ], 75 | dataClass: WorkflowData::class, 76 | ); 77 | } 78 | 79 | public function update(string|int $workflowId, StoryblokData $storyblokData): StoryblokResponseInterface 80 | { 81 | return $this->makeRequest( 82 | "POST", 83 | "/v1/spaces/" . $this->spaceId . '/workflows/' . $workflowId, 84 | [ 85 | "body" => $storyblokData->toArray(), 86 | ], 87 | dataClass: WorkflowData::class, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Data/ComponentFolder.php: -------------------------------------------------------------------------------- 1 | data = []; 19 | $this->data['name'] = $name; 20 | $this->data['parent_id'] = $parentId; 21 | } 22 | 23 | /** 24 | * @param mixed[] $data 25 | * @throws StoryblokFormatException 26 | */ 27 | public static function make(array $data = []): self 28 | { 29 | $dataObject = new StoryblokData($data); 30 | if (!($dataObject->hasKey('name'))) { 31 | throw new StoryblokFormatException("Component Folder is not valid, missing the name"); 32 | } 33 | 34 | $componentFolder = new ComponentFolder( 35 | $dataObject->getString("name"), 36 | $dataObject->getString("parent_id") 37 | ); 38 | $componentFolder->setData($dataObject->toArray()); 39 | // validate 40 | if (! $componentFolder->isValid()) { 41 | if ($dataObject->getString("name") === "") { 42 | throw new StoryblokFormatException("Component Folder is not valid"); 43 | } 44 | 45 | throw new StoryblokFormatException("Component Folder <" . $dataObject->getString("name") . "> is not valid"); 46 | } 47 | 48 | return $componentFolder; 49 | 50 | } 51 | 52 | public function setName(string $name): void 53 | { 54 | $this->set('name', $name); 55 | } 56 | 57 | /** 58 | * Technical name used for component property in entries 59 | */ 60 | public function name(): string 61 | { 62 | return $this->getString('name'); 63 | } 64 | 65 | /** 66 | * The numeric ID in string format "12345678" 67 | */ 68 | public function id(): string 69 | { 70 | return $this->getString('id'); 71 | } 72 | 73 | /** 74 | * The numeric parent ID in string format "12345678" 75 | */ 76 | public function parentId(): string 77 | { 78 | return $this->getString('parent_id'); 79 | } 80 | 81 | /** 82 | * The parent UUID in string format "12345678" 83 | */ 84 | public function parentUuid(): string 85 | { 86 | return $this->getString('parent_uuid'); 87 | } 88 | 89 | public function uuid(): string 90 | { 91 | return $this->getString('uuid'); 92 | } 93 | 94 | /** 95 | * Validates if the component data contains all required fields and valid values 96 | */ 97 | public function isValid(): bool 98 | { 99 | return $this->hasKey('name'); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Endpoints/WorkflowStageApi.php: -------------------------------------------------------------------------------- 1 | $params->toArray(), 35 | ]; 36 | 37 | return $this->makeRequest( 38 | "GET", 39 | '/v1/spaces/' . $this->spaceId . '/workflow_stages', 40 | options: $options, 41 | dataClass: WorkflowStagesData::class, 42 | ); 43 | } 44 | 45 | public function get(string|int $workflowStageId): StoryblokResponseInterface 46 | { 47 | 48 | return $this->makeRequest( 49 | "GET", 50 | '/v1/spaces/' . $this->spaceId . '/workflow_stages/' . $workflowStageId, 51 | dataClass: WorkflowStageData::class, 52 | ); 53 | } 54 | 55 | /** 56 | * @param string|int $workflowStageId the workflow stage identifier 57 | */ 58 | public function delete(string|int $workflowStageId): StoryblokResponseInterface 59 | { 60 | return $this->makeRequest( 61 | "DELETE", 62 | '/v1/spaces/' . $this->spaceId . '/workflow_stages/' . $workflowStageId, 63 | dataClass: WorkflowStageData::class, 64 | ); 65 | } 66 | 67 | public function create(StoryblokData $storyblokData): StoryblokResponseInterface 68 | { 69 | return $this->makeRequest( 70 | "POST", 71 | "/v1/spaces/" . $this->spaceId . '/workflow_stages', 72 | [ 73 | "body" => [ 74 | "workflow_stage" => $storyblokData->toArray(), 75 | ], 76 | ], 77 | dataClass: WorkflowStageData::class, 78 | ); 79 | } 80 | 81 | public function update(string|int $workflowStageId, StoryblokData $storyblokData): StoryblokResponseInterface 82 | { 83 | return $this->makeRequest( 84 | "POST", 85 | "/v1/spaces/" . $this->spaceId . '/workflow_stages/' . $workflowStageId, 86 | [ 87 | "body" => $storyblokData->toArray(), 88 | ], 89 | dataClass: WorkflowStageData::class, 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Endpoints/WorkflowStageChangeApi.php: -------------------------------------------------------------------------------- 1 | toArray(); 31 | $paramsArray["page"] = $page; 32 | $paramsArray["per_page"] = $perPage; 33 | $options = [ 34 | "query" => $paramsArray, 35 | ]; 36 | $httpResponse = $this->makeHttpRequest( 37 | "GET", 38 | "/v1/spaces/" . $this->spaceId . "/workflow_stage_changes", 39 | options: $options, 40 | ); 41 | 42 | return new WorkflowStageChangesResponse($httpResponse); 43 | } 44 | 45 | /** 46 | * 47 | * @param int|int[] $assignSpaceRoleIds 48 | * @param int|int[] $assignUserIds 49 | */ 50 | public function create( 51 | WorkflowStageChange $workflowStageChange, 52 | string|int|null $releaseId = "0", 53 | ?bool $notify = false, 54 | ?string $commentMessage = "", 55 | int|array $assignSpaceRoleIds = [], 56 | int|array $assignUserIds = [], 57 | ): WorkflowStageChangeResponse { 58 | $body = [ 59 | "workflow_stage_change" => $workflowStageChange->toArray(), 60 | ]; 61 | if (!is_null($releaseId)) { 62 | $body["release_id"] = $releaseId; 63 | } 64 | 65 | if (!is_null($notify)) { 66 | $body["notify"] = $notify; 67 | } 68 | 69 | if (!is_null($commentMessage)) { 70 | $body["comment"] = []; 71 | $body["comment"]["message"] = $commentMessage; 72 | } 73 | 74 | $body["assign"] = []; 75 | $body["assign"]["space_role_ids"] = $assignSpaceRoleIds; 76 | $body["assign"]["user_ids"] = $assignUserIds; 77 | 78 | $httpResponse = $this->makeHttpRequest( 79 | "POST", 80 | "/v1/spaces/" . $this->spaceId . "/workflow_stage_changes", 81 | [ 82 | "body" => $body, 83 | ], 84 | ); 85 | 86 | return new WorkflowStageChangeResponse($httpResponse); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/StoryblokUtils.php: -------------------------------------------------------------------------------- 1 | [0, 999_999], 11 | // 'CN' => [0, 1_000_000], 12 | 'US' => [1_000_000, 1_999_999], 13 | 'CA' => [2_000_000, 2_999_999], 14 | 'AP' => [3_000_000, 3_999_999], 15 | ]; 16 | 17 | public static function getRegionFromSpaceId(string|int $spaceId): string 18 | { 19 | foreach (self::ALL_REGION_RANGES as $region => [$min, $max]) { 20 | if ($spaceId >= $min && $spaceId < $max) { 21 | return $region; 22 | } 23 | } 24 | 25 | return 'EU'; // fallback in case the ID doesn't match any range 26 | } 27 | 28 | /** 29 | * Each type of storyblok service plan 30 | * (Community, Business, Enterprise, etc.) is internally coded by 31 | * an integer number. 32 | * The function returns the formal description of the plan 33 | * related to the code. 34 | */ 35 | public static function getPlanDescription(int|string $planLevel): string 36 | { 37 | return (string) match ($planLevel) { 38 | 39 | 0, "0" => 'Starter (Trial)', 40 | 2, "2" => 'Pro Space', 41 | 1,"1" => 'Standard Space', 42 | 1000, "1000" => 'Development', 43 | 100, "100" => 'Community', 44 | 1100, "1100" => 'Starter (Plan 1)', 45 | 200, "200" => 'Entry', 46 | 999, "999" => 'Development Plan', 47 | 1200, "1200" => "Growth (Plan 2i)", 48 | 1300, "1300" => "Growth Plus (Plan 2ii)", 49 | 300, "300" => 'Teams', 50 | 301, "301" => 'Business', 51 | 1400, "1400" => "Premium (Plan 3)", 52 | 1401, "1401" => "Premium CN (Plan 3 CN)", 53 | 1500, "1500" => "Elite (Plan 4)", 54 | 1501, "1501" => "Elite CN (Plan 4 CN)", 55 | 400, "400" => 'Enterprise', 56 | 500, "500" => 'Enterprise Plus', 57 | 501, "501" => 'Enterprise Essentials', 58 | 502, "502" => 'Enterprise Scale', 59 | 503, "503" => 'Enterprise Ultimate', 60 | default => $planLevel, 61 | }; 62 | 63 | } 64 | 65 | public static function baseUriFromRegionForMapi(string $region): string 66 | { 67 | return match ($region) { 68 | "US" => "https://api-us.storyblok.com", 69 | "CA" => "https://api-ca.storyblok.com", 70 | "AP" => "https://api-ap.storyblok.com", 71 | "CN" => "https://app.storyblokchina.cn", 72 | default => "https://mapi.storyblok.com", 73 | }; 74 | } 75 | 76 | public static function baseUriFromRegionForOauth(string $region): string 77 | { 78 | return match ($region) { 79 | "US" => "https://api-us.storyblok.com", 80 | "CA" => "https://api-ca.storyblok.com", 81 | "AP" => "https://api-ap.storyblok.com", 82 | "CN" => "https://app.storyblokchina.cn", 83 | default => "https://api.storyblok.com", 84 | }; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Data/Asset.php: -------------------------------------------------------------------------------- 1 | data = []; 17 | $this->data["filename"] = $filename; 18 | $this->data["fieldtype"] = "asset"; 19 | } 20 | 21 | /** 22 | * The Asset data response payload doesn't have the typical 23 | * "asset" attribute (like the story, the space etc) 24 | * @param mixed[] $data 25 | * @throws StoryblokFormatException 26 | */ 27 | public static function make(array $data = []): self 28 | { 29 | $dataObject = new StoryblokData($data); 30 | if (!$dataObject->hasKey("fieldtype")) { 31 | $dataObject->set("fieldtype", "asset"); 32 | } 33 | 34 | if ( 35 | !( 36 | $dataObject->hasKey("filename") && 37 | $dataObject->hasKey("fieldtype") 38 | ) 39 | ) { 40 | // is not valid 41 | } 42 | 43 | $asset = new Asset($dataObject->getString("filename")); 44 | $asset->setData($dataObject->toArray()); 45 | // validate 46 | if (!$asset->isValid()) { 47 | throw new StoryblokFormatException("Asset is not valid"); 48 | } 49 | 50 | return $asset; 51 | } 52 | 53 | public function isValid(): bool 54 | { 55 | return $this->hasKey("filename"); 56 | } 57 | 58 | public function filenameCDN(): string 59 | { 60 | return str_replace( 61 | "https://s3.amazonaws.com/a.storyblok.com", 62 | "https://a.storyblok.com", 63 | $this->filename(), 64 | ); 65 | } 66 | 67 | public function contentType(): string 68 | { 69 | return $this->getString("content_type"); 70 | } 71 | 72 | public function contentLength(): int|null 73 | { 74 | return $this->getInt("content_length"); 75 | } 76 | 77 | public function createdAt(): null|string 78 | { 79 | return $this->getFormattedDateTime("created_at", "", format: "Y-m-d"); 80 | } 81 | 82 | public function updatedAt(): null|string 83 | { 84 | return $this->getFormattedDateTime("updated_at", "", format: "Y-m-d"); 85 | } 86 | 87 | public function setExternalUrl(string $url): self 88 | { 89 | $this->set("filename", $url); 90 | $this->set("is_external_url", true); 91 | return $this; 92 | } 93 | 94 | public static function emptyAsset(): Asset 95 | { 96 | return self::make([ 97 | "id" => null, 98 | "alt" => "", 99 | "name" => "", 100 | "focus" => "", 101 | "title" => "", 102 | "source" => "", 103 | "filename" => "", 104 | "copyright" => "", 105 | "fieldtype" => "asset", 106 | "meta_data" => (object) [], 107 | ]); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Data/StoryCollectionItem.php: -------------------------------------------------------------------------------- 1 | data = []; 27 | 28 | } 29 | 30 | /** 31 | * @param mixed[] $data 32 | * @throws StoryblokFormatException 33 | */ 34 | public static function make(array $data = []): self 35 | { 36 | $dataObject = new StoryblokData($data); 37 | if (!($dataObject->hasKey('name') && $dataObject->hasKey('slug'))) { 38 | // is not valid 39 | } 40 | 41 | $storyItem = new StoryCollectionItem(); 42 | $storyItem->setData($dataObject->toArray()); 43 | // validate 44 | if (! $storyItem->isValid()) { 45 | throw new StoryblokFormatException("Story is not valid"); 46 | } 47 | 48 | return $storyItem; 49 | 50 | } 51 | 52 | public function name(): string 53 | { 54 | return $this->getString('name'); 55 | } 56 | 57 | public function createdAt(string $format = "Y-m-d"): null|string 58 | { 59 | return $this->getFormattedDateTime('created_at', "", format: $format); 60 | } 61 | 62 | public function publishedAt(string $format = "Y-m-d"): null|string 63 | { 64 | return $this->getFormattedDateTime('published_at', "", format: $format); 65 | } 66 | 67 | public function updatedAt(): null|string 68 | { 69 | return $this->getFormattedDateTime('updated_at', "", format: "Y-m-d"); 70 | } 71 | 72 | public function id(): string 73 | { 74 | return $this->getString('id'); 75 | } 76 | 77 | public function uuid(): string 78 | { 79 | return $this->getString('uuid'); 80 | } 81 | 82 | /** 83 | * Validates if the story data contains all required fields and valid values 84 | */ 85 | public function isValid(): bool 86 | { 87 | if (!$this->hasKey('name') || in_array($this->getString('name'), ['', '0'], true)) { 88 | return false; 89 | } 90 | 91 | return $this->hasKey('slug') && !in_array($this->getString('slug'), ['', '0'], true); 92 | } 93 | 94 | /** 95 | * Set tags for Story, from a `Tags` collection 96 | * @return $this 97 | */ 98 | public function setTags(Tags $tags): self 99 | { 100 | 101 | return $this->setTagsFromArray($tags->getTagsArray()); 102 | 103 | } 104 | 105 | /** 106 | * Set tags for Story, from a string of arrays like ["tag1", "tag2"] 107 | * @param string[] $tagsArray 108 | * @return $this 109 | */ 110 | public function setTagsFromArray(array $tagsArray): self 111 | { 112 | $this->set("tag_list", $tagsArray); 113 | return $this; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Data/StoryComponent.php: -------------------------------------------------------------------------------- 1 | data = []; 38 | $this->data["component"] = $contentType; 39 | } 40 | 41 | /** 42 | * @param mixed[] $data 43 | * @throws StoryblokFormatException 44 | */ 45 | public static function make(array $data = []): self 46 | { 47 | if (!array_key_exists("component", $data)) { 48 | throw new StoryblokFormatException( 49 | "Story `content` is not valid, `component` property is missing.", 50 | ); 51 | } 52 | 53 | $storyContent = new StoryComponent($data["component"]); 54 | $storyContent->setData($data); 55 | // validate 56 | if (!$storyContent->isValid()) { 57 | throw new StoryblokFormatException("Story content is not valid"); 58 | } 59 | 60 | return $storyContent; 61 | } 62 | 63 | public function setComponent(string $component): self 64 | { 65 | $this->set("component", $component); 66 | return $this; 67 | } 68 | 69 | public function component(): string 70 | { 71 | return $this->getString("component"); 72 | } 73 | 74 | /** 75 | * Validates if the story content contains all required fields and valid values 76 | */ 77 | public function isValid(): bool 78 | { 79 | return $this->hasKey("component"); 80 | } 81 | 82 | public function setAsset(string $field, Asset $asset): self 83 | { 84 | $this->setAssetField($field, AssetField::makeFromAsset($asset)); 85 | ///$this->set($field, $asset->toArray()); 86 | return $this; 87 | } 88 | 89 | /** 90 | * Set an asset field on the content object. 91 | * 92 | * Accepts a field name and an AssetField instance, converts the AssetField 93 | * into an array (via `toArray()`), and stores it using the underlying `set()` method. 94 | * 95 | * @param string $field The name of the field to set (e.g. "image"). 96 | * @param AssetField $assetField The AssetField instance to assign. 97 | * 98 | * @return $this Returns the current object for method chaining. 99 | * 100 | * @example 101 | * $content->setAssetField( 102 | * 'image', 103 | * AssetField::makeFromAsset($assets) 104 | * ); 105 | * 106 | * // Or indirectly through your wrapper: 107 | * $content->set('image', AssetField::makeFromAsset($assets)->toArray()); 108 | */ 109 | public function setAssetField(string $field, AssetField $assetField): self 110 | { 111 | $this->set($field, $assetField->toArray()); 112 | return $this; 113 | } 114 | 115 | public function addBlock(string $field, StoryComponent $component): self 116 | { 117 | $blocks = $this->getArray($field); 118 | $blocks[] = $component->toArray(); 119 | $this->set($field, $blocks); 120 | return $this; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Endpoints/SpaceApi.php: -------------------------------------------------------------------------------- 1 | makeHttpRequest( 23 | "GET", 24 | self::API_PATH_SPACE_PREFIX_V1, 25 | ); 26 | return new SpacesResponse($httpResponse, Spaces::class); 27 | } 28 | 29 | /** 30 | * @throws TransportExceptionInterface 31 | */ 32 | public function get(string $spaceId): SpaceResponse 33 | { 34 | $httpResponse = $this->makeHttpRequest( 35 | "GET", 36 | self::buildSpacesEndpoint($spaceId), 37 | ); 38 | 39 | return new SpaceResponse($httpResponse); 40 | } 41 | 42 | public function create(Space $spaceData): SpaceResponse 43 | { 44 | 45 | $httpResponse = $this->makeHttpRequest( 46 | "POST", 47 | self::API_PATH_SPACE_PREFIX_V1, 48 | [ 49 | "body" => [ 50 | "space" => $spaceData->toArray(), 51 | ], 52 | ], 53 | ); 54 | return new SpaceResponse($httpResponse); 55 | } 56 | 57 | public function update(string $spaceId, Space $spaceData): SpaceResponse 58 | { 59 | $this->validateSpaceId($spaceId); 60 | //$this->validateSpaceData($spaceData); 61 | $httpResponse = $this->makeHttpRequest( 62 | "PUT", 63 | $this->buildSpacesEndpoint($spaceId), 64 | [ 65 | "body" => json_encode(["space" => $spaceData->toArray()]), 66 | ] 67 | ); 68 | return new SpaceResponse($httpResponse); 69 | } 70 | 71 | public function duplicate(string|int $duplicateId, string $name, bool $inOrg = false): SpaceResponse 72 | { 73 | $body = [ 74 | "dup_id" => $duplicateId, 75 | "space" => [ 76 | "name" => $name, 77 | ], 78 | ]; 79 | if ($inOrg) { 80 | $body["in_org"] = true; 81 | } 82 | 83 | $httpResponse = $this->makeHttpRequest( 84 | "POST", 85 | self::API_PATH_SPACE_PREFIX_V1, 86 | [ 87 | "body" => $body, 88 | ], 89 | ); 90 | return new SpaceResponse($httpResponse); 91 | } 92 | 93 | /** 94 | * @param $spaceId 95 | */ 96 | public function delete(string $spaceId): SpaceResponse 97 | { 98 | $httpResponse = $this->makeHttpRequest( 99 | "DELETE", 100 | self::API_PATH_SPACE_PREFIX_V1 . '/' . $spaceId, 101 | ); 102 | return new SpaceResponse($httpResponse); 103 | } 104 | 105 | /** 106 | * @param $spaceId 107 | */ 108 | public function backup(string $spaceId): SpaceResponse 109 | { 110 | $httpResponse = $this->makeHttpRequest( 111 | "POST", 112 | sprintf('%s/%s/backups', self::API_PATH_SPACE_PREFIX_V1, $spaceId), 113 | [ 114 | "body" => [ 115 | ], 116 | ], 117 | ); 118 | return new SpaceResponse($httpResponse); 119 | } 120 | 121 | private function buildSpacesEndpoint(string $spaceId): string 122 | { 123 | return sprintf('%s/%s', self::API_PATH_SPACE_PREFIX_V1, $spaceId); 124 | } 125 | 126 | /** 127 | * Validates space ID 128 | * 129 | * @throws \InvalidArgumentException 130 | */ 131 | private function validateSpaceId(string $spaceId): void 132 | { 133 | if ($spaceId === '' || $spaceId === '0') { 134 | throw new \InvalidArgumentException('Space ID cannot be empty'); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Data/Space.php: -------------------------------------------------------------------------------- 1 | data = []; 19 | $this->data["name"] = $name; 20 | } 21 | 22 | /** 23 | * @param mixed[] $data 24 | * @throws StoryblokFormatException 25 | */ 26 | public static function make(array $data = []): self 27 | { 28 | $dataObject = new StoryblokData($data); 29 | $name = $dataObject->getString("name"); 30 | $space = new self($dataObject->getString("name")); 31 | $space->setData($dataObject->toArray()); 32 | // validate 33 | if ($space->name() !== $name) { 34 | throw new StoryblokFormatException("Space has no name"); 35 | } 36 | 37 | return $space; 38 | } 39 | 40 | public function setName(string $name): void 41 | { 42 | $this->set("name", $name); 43 | } 44 | 45 | public function setDomain(string $domain): void 46 | { 47 | $this->set("domain", $domain); 48 | } 49 | 50 | public function name(): string 51 | { 52 | return $this->getString("name", ""); 53 | } 54 | 55 | public function region(): string 56 | { 57 | return $this->getString("region", ""); 58 | } 59 | 60 | public function id(): string 61 | { 62 | return $this->getString("id", ""); 63 | } 64 | 65 | /** 66 | * Retrieves the domain associated with the Space. 67 | * 68 | * Returns the value stored under the "domain" key as a string. 69 | * 70 | * @return string The domain name. 71 | */ 72 | public function domain(): string 73 | { 74 | return $this->getString("domain"); 75 | } 76 | 77 | /** 78 | * Retrieves the first token associated with the Space. 79 | * 80 | * Returns the value stored under the "first_token" key. 81 | * 82 | * @return string The first token, or an empty string if none is defined. 83 | */ 84 | public function firstToken(): string 85 | { 86 | return $this->getString("first_token", ""); 87 | } 88 | 89 | public function environments(): SpaceEnvironments 90 | { 91 | return SpaceEnvironments::make($this->getArray("environments")); 92 | } 93 | 94 | public function addEnvironment( 95 | SpaceEnvironment $spaceEnvironment, 96 | ): SpaceEnvironments { 97 | $environments = $this->getArray("environments"); 98 | $environments[] = $spaceEnvironment->toArray(); 99 | $this->set("environments", $environments); 100 | 101 | return SpaceEnvironments::make($environments); 102 | } 103 | 104 | public function createdAt(): null|string 105 | { 106 | return $this->getFormattedDateTime("created_at", "", format: "Y-m-d"); 107 | } 108 | 109 | public function updatedAt(): null|string 110 | { 111 | return $this->getFormattedDateTime("updated_at", "", format: "Y-m-d"); 112 | } 113 | 114 | public function planLevel(): string 115 | { 116 | return $this->getString("plan_level"); 117 | } 118 | 119 | public function planDescription(): null|string 120 | { 121 | return StoryblokUtils::getPlanDescription($this->planLevel()); 122 | } 123 | 124 | public function ownerId(): string 125 | { 126 | return $this->getString("owner_id", ""); 127 | } 128 | 129 | /** 130 | * Determines whether the current entity is owned by the given user. 131 | * 132 | * Compares the space's owner ID with the ID of the provided user instance. 133 | * 134 | * @param User $user The user to check ownership against. 135 | * @return bool True if the user owns the entity, false otherwise. 136 | */ 137 | public function isOwnedByUser(User $user): bool 138 | { 139 | return $this->ownerId() === $user->id(); 140 | } 141 | 142 | /** 143 | * Checks whether the entity is marked as a demo instance. 144 | * 145 | * Retrieves the `is_demo` flag and returns its boolean value. 146 | * 147 | * @return bool True if the entity is marked as a demo, false otherwise. 148 | */ 149 | public function isDemo(): bool 150 | { 151 | return $this->getBoolean("is_demo", false); 152 | } 153 | 154 | /** 155 | * Remove the Demo mode flag 156 | */ 157 | public function removeDemoMode(): void 158 | { 159 | $this->set("is_demo", false); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/ManagementApiClient.php: -------------------------------------------------------------------------------- 1 | value); 48 | $httpClient = HttpClient::create(); 49 | if ($shouldRetry) { 50 | $httpClient = new RetryableHttpClient( 51 | $httpClient, 52 | new GenericRetryStrategy([429]), 53 | ); 54 | } 55 | 56 | $this->httpClient = $httpClient->withOptions([ 57 | "base_uri" => $baseUriMapi, 58 | "headers" => [ 59 | "Accept" => "application/json", 60 | "Content-Type" => "application/json", 61 | "Authorization" => $personalAccessToken, 62 | ], 63 | ]); 64 | $this->httpAssetClient = HttpClient::create(); 65 | } 66 | 67 | /** 68 | * Initialize the MapiClient 69 | */ 70 | public static function init( 71 | string $personalAccessToken, 72 | Region $region = Region::EU, 73 | ?string $baseUri = null, 74 | ): self { 75 | return new self($personalAccessToken, $region, $baseUri); 76 | } 77 | 78 | public static function initTest( 79 | HttpClientInterface $httpClient, 80 | ?HttpClientInterface $httpAssetClient = null, 81 | ): self { 82 | $client = new self(""); 83 | //$baseUriMapi = $baseUri ?? StoryblokUtils::baseUriFromRegionForMapi($region); 84 | 85 | $client->httpClient = $httpClient; 86 | if ( 87 | $httpAssetClient instanceof 88 | \Symfony\Contracts\HttpClient\HttpClientInterface 89 | ) { 90 | $client->httpAssetClient = $httpAssetClient; 91 | } else { 92 | $client->httpAssetClient = new MockHttpClient(); 93 | } 94 | 95 | return $client; 96 | } 97 | 98 | public function httpClient(): HttpClientInterface 99 | { 100 | return $this->httpClient; 101 | } 102 | 103 | public function httpAssetClient(): HttpClientInterface 104 | { 105 | return $this->httpAssetClient; 106 | } 107 | 108 | public function storyApi( 109 | string|int $spaceId, 110 | ?LoggerInterface $logger = null, 111 | ): StoryApi { 112 | return new StoryApi($this, $spaceId, $logger ?? new NullLogger()); 113 | } 114 | 115 | public function storyBulkApi( 116 | string|int $spaceId, 117 | ?LoggerInterface $logger = null, 118 | ): StoryBulkApi { 119 | return new StoryBulkApi($this, $spaceId, $logger ?? new NullLogger()); 120 | } 121 | 122 | public function assetApi(string|int $spaceId): AssetApi 123 | { 124 | return new AssetApi($this, $spaceId); 125 | } 126 | 127 | public function tagApi(string|int $spaceId): TagApi 128 | { 129 | return new TagApi($this, $spaceId); 130 | } 131 | 132 | public function workflowApi(string|int $spaceId): WorkflowApi 133 | { 134 | return new WorkflowApi($this, $spaceId); 135 | } 136 | 137 | public function workflowStageApi(string|int $spaceId): WorkflowStageApi 138 | { 139 | return new WorkflowStageApi($this, $spaceId); 140 | } 141 | 142 | public function componentApi( 143 | string|int $spaceId, 144 | ?LoggerInterface $logger = null, 145 | ): ComponentApi { 146 | return new ComponentApi($this, $spaceId, $logger ?? new NullLogger()); 147 | } 148 | 149 | public function managementApi(): ManagementApi 150 | { 151 | return new ManagementApi($this); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Endpoints/ComponentApi.php: -------------------------------------------------------------------------------- 1 | toArray() : []; 49 | 50 | $options = [ 51 | 'query' => $paramsArray, 52 | ]; 53 | 54 | $httpResponse = $this->makeHttpRequest( 55 | "GET", 56 | $this->buildComponentsEndpoint(), 57 | options: $options 58 | ); 59 | return new ComponentsResponse($httpResponse); 60 | } 61 | 62 | /** 63 | * Retrieves a specific component by ID 64 | * 65 | * @throws StoryblokApiException 66 | */ 67 | public function get(string $componentId): ComponentResponse 68 | { 69 | $this->validateComponentId($componentId); 70 | 71 | $httpResponse = $this->makeHttpRequest( 72 | "GET", 73 | $this->buildComponentEndpoint($componentId), 74 | ); 75 | return new ComponentResponse($httpResponse); 76 | 77 | } 78 | 79 | /** 80 | * Creates a new component 81 | * 82 | * @throws InvalidComponentDataException 83 | * @throws StoryblokApiException 84 | * @throws TransportExceptionInterface 85 | */ 86 | public function create(Component $componentData): ComponentResponse 87 | { 88 | $this->validateComponentData($componentData); 89 | 90 | $httpResponse = $this->makeHttpRequest( 91 | "POST", 92 | $this->buildComponentsEndpoint(), 93 | [ 94 | "body" => json_encode(["component" => $componentData->toArray()]), 95 | ] 96 | ); 97 | 98 | return new ComponentResponse($httpResponse); 99 | 100 | } 101 | 102 | /** 103 | * Updates an existing component 104 | * 105 | * @throws InvalidComponentDataException 106 | */ 107 | public function update(string $componentId, Component $componentData): ComponentResponse 108 | { 109 | $this->validateComponentId($componentId); 110 | //$this->validateStoryData($storyData); 111 | 112 | $httpResponse = $this->makeHttpRequest( 113 | "PUT", 114 | $this->buildComponentEndpoint($componentId), 115 | [ 116 | "body" => json_encode(["component" => $componentData->toArray()]), 117 | ] 118 | ); 119 | return new ComponentResponse($httpResponse); 120 | } 121 | 122 | /** 123 | * Validates component ID 124 | * 125 | * @throws \InvalidArgumentException 126 | */ 127 | private function validateComponentId(string $componentId): void 128 | { 129 | if ($componentId === '' || $componentId === '0') { 130 | throw new \InvalidArgumentException('Component ID cannot be empty'); 131 | } 132 | } 133 | 134 | /** 135 | * Validates component data 136 | * 137 | * @throws InvalidComponentDataException 138 | */ 139 | private function validateComponentData(Component $componentData): void 140 | { 141 | if (!$componentData->isValid()) { 142 | throw new InvalidStoryDataException('Invalid component data provided'); 143 | } 144 | } 145 | 146 | /** 147 | * Builds the base endpoint for components 148 | */ 149 | private function buildComponentsEndpoint(): string 150 | { 151 | return sprintf('/v1/spaces/%s/components', $this->spaceId); 152 | } 153 | 154 | /** 155 | * Builds the endpoint for a specific component 156 | */ 157 | private function buildComponentEndpoint(string $componentId): string 158 | { 159 | return sprintf('%s/%s', $this->buildComponentsEndpoint(), $componentId); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Data/Story.php: -------------------------------------------------------------------------------- 1 | data = []; 22 | $this->data["name"] = $name; 23 | $this->data["slug"] = $slug; 24 | $this->data["content"] = $content->toArray(); 25 | } 26 | 27 | /** 28 | * @param mixed[] $data 29 | * @throws StoryblokFormatException 30 | */ 31 | public static function make(array $data = []): self 32 | { 33 | $dataObject = new StoryblokData($data); 34 | if ( 35 | !( 36 | $dataObject->hasKey("name") && 37 | $dataObject->hasKey("slug") && 38 | $dataObject->hasKey("content") && 39 | $dataObject->hasKey("content.component") 40 | ) 41 | ) { 42 | // is not valid 43 | } 44 | 45 | $content = StoryComponent::make($dataObject->getArray("content")); 46 | 47 | $story = new Story( 48 | $dataObject->getString("name"), 49 | $dataObject->getString("slug"), 50 | $content, 51 | ); 52 | $story->setData($dataObject->toArray()); 53 | // validate 54 | if (!$story->isValid()) { 55 | throw new StoryblokFormatException("Story is not valid"); 56 | } 57 | 58 | return $story; 59 | } 60 | 61 | public function setName(string $name): void 62 | { 63 | $this->set("name", $name); 64 | } 65 | 66 | public function setSlug(string $slug): void 67 | { 68 | $this->set("slug", $slug); 69 | } 70 | 71 | public function setCreatedAt(string $createdAt): void 72 | { 73 | $this->set("created_at", $createdAt); 74 | } 75 | 76 | public function setContent(StoryComponent $content): void 77 | { 78 | $this->set("content", $content->toArray()); 79 | } 80 | 81 | public function content(): StoryComponent 82 | { 83 | $contentArray = $this->getArray("content"); 84 | return StoryComponent::make($contentArray); 85 | } 86 | 87 | public function name(): string 88 | { 89 | return $this->getString("name"); 90 | } 91 | 92 | /** 93 | * Get the folder id for the Story. 94 | * 95 | * @return int the identifier of the parent folder, 0 if the story is stored at the root level 96 | */ 97 | public function folderId(): int 98 | { 99 | return (int) $this->getInt("parent_id", 0); 100 | } 101 | 102 | public function createdAt(string $format = "Y-m-d"): null|string 103 | { 104 | return $this->getFormattedDateTime("created_at", "", format: $format); 105 | } 106 | 107 | public function publishedAt(string $format = "Y-m-d"): null|string 108 | { 109 | return $this->getFormattedDateTime("published_at", "", format: $format); 110 | } 111 | 112 | public function updatedAt(): null|string 113 | { 114 | return $this->getFormattedDateTime("updated_at", "", format: "Y-m-d"); 115 | } 116 | 117 | public function setContentType(string $componentName): self 118 | { 119 | $this->defaultContentType = $componentName; 120 | return $this; 121 | } 122 | 123 | public function defaultContentType(): string 124 | { 125 | return $this->defaultContentType; 126 | } 127 | 128 | public function id(): string 129 | { 130 | return $this->getString("id"); 131 | } 132 | 133 | public function uuid(): string 134 | { 135 | return $this->getString("uuid"); 136 | } 137 | 138 | /** 139 | * Validates if the story data contains all required fields and valid values 140 | */ 141 | public function isValid(): bool 142 | { 143 | if ( 144 | !$this->hasKey("name") || 145 | in_array($this->getString("name"), ["", "0"], true) 146 | ) { 147 | return false; 148 | } 149 | 150 | return $this->hasKey("slug") && 151 | !in_array($this->getString("slug"), ["", "0"], true); 152 | } 153 | 154 | /** 155 | * Set tags for Story, from a `Tags` collection 156 | * @return $this 157 | */ 158 | public function setTags(Tags $tags): self 159 | { 160 | return $this->setTagsFromArray($tags->getTagsArray()); 161 | } 162 | 163 | /** 164 | * Set tags for Story, from a string of arrays like ["tag1", "tag2"] 165 | * @param string[] $tagsArray 166 | * @return $this 167 | */ 168 | public function setTagsFromArray(array $tagsArray): self 169 | { 170 | $this->set("tag_list", $tagsArray); 171 | return $this; 172 | } 173 | 174 | /** 175 | * Set the folder for the Story. 176 | * 177 | * @param int|string $folderId identifier of the Folder where to store the story 178 | * @return $this 179 | */ 180 | public function setFolderId(int|string $folderId): self 181 | { 182 | $this->set("parent_id", (int) $folderId); 183 | return $this; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Response/StoryblokResponse.php: -------------------------------------------------------------------------------- 1 | response; 32 | } 33 | 34 | public function data(): StoryblokDataInterface 35 | { 36 | if (method_exists($this->dataClass, "makeFromResponse")) { 37 | return $this->dataClass::makeFromResponse($this->toArray()); 38 | } 39 | 40 | return $this->dataClass::make($this->toArray()); 41 | } 42 | 43 | public function getResponseBody(): string 44 | { 45 | return $this->response->getContent(false); 46 | } 47 | 48 | public function getResponseHeaders(): void 49 | { 50 | $this->response->getHeaders(); 51 | } 52 | 53 | public function getHeader(string $headerName): mixed 54 | { 55 | try { 56 | $headers = $this->response->getHeaders(); 57 | 58 | if ( 59 | array_key_exists($headerName, $headers) && 60 | array_key_exists(0, $headers[$headerName]) 61 | ) { 62 | return $headers[$headerName][0]; 63 | } 64 | } catch (ClientExceptionInterface) { 65 | } 66 | 67 | return null; 68 | } 69 | 70 | public function getHeaderInt(string $headerName): int|null 71 | { 72 | $value = $this->getHeader($headerName); 73 | return is_numeric($value) ? (int) $value : null; 74 | } 75 | 76 | public function total(): int|null 77 | { 78 | return $this->getHeaderInt("total"); 79 | } 80 | 81 | public function perPage(): int|null 82 | { 83 | return $this->getHeaderInt("per-page"); 84 | } 85 | 86 | public function getResponseStatusCode(): int 87 | { 88 | return $this->response->getStatusCode(); 89 | } 90 | 91 | public function getLastCalledUrl(): string 92 | { 93 | if (is_scalar($this->response->getInfo("url"))) { 94 | return strval($this->response->getInfo("url")); 95 | } 96 | 97 | return ""; 98 | } 99 | 100 | public function isOk(): bool 101 | { 102 | return $this->getResponseStatusCode() >= 200 && 103 | $this->getResponseStatusCode() < 300; 104 | } 105 | 106 | public function getErrorMessage(): string 107 | { 108 | if ($this->isOk()) { 109 | return "No error detected, HTTP Status Code: " . 110 | $this->getResponseStatusCode(); 111 | } 112 | 113 | try { 114 | $data = $this->data(); 115 | $message = $data->getString("error"); 116 | if ($message !== "" && $message !== "0") { 117 | return $message; 118 | } 119 | } catch (\Exception) { 120 | } 121 | 122 | $message = match ($this->getResponseStatusCode()) { 123 | 400 124 | => "Bad Request. Wrong format was sent (eg. XML instead of JSON).", 125 | 401 => "Unauthorized. No valid API key provided.", 126 | 403 => "Forbidden. Insufficient permissions.", 127 | 404 128 | => "Not Found. The requested resource doesn't exist (perhaps due to not yet published content entries).", 129 | 422 130 | => "Unprocessable Entity. The request cannot be processed because it is invalid, for example, due to a missing required parameter or a duplicate key.", 131 | 429 132 | => "Too many requests. Too many requests hit the API too quickly. We recommend an exponential backoff (throttling) of your requests.", 133 | 500, 134 | 502, 135 | 503, 136 | 504 137 | => "Server error. We are unable to process your request.", 138 | default => "Unknown error", 139 | }; 140 | return $this->getResponseStatusCode() . " - " . $message; 141 | } 142 | 143 | public function asJson(): string|false 144 | { 145 | return json_encode($this->toArray()); 146 | } 147 | 148 | /** 149 | * @return array 150 | * @throws ClientExceptionInterface 151 | * @throws RedirectionExceptionInterface 152 | * @throws ServerExceptionInterface 153 | * @throws TransportExceptionInterface 154 | * @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface 155 | */ 156 | public function toArray(): array 157 | { 158 | return $this->response->toArray(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Endpoints/AssetApi.php: -------------------------------------------------------------------------------- 1 | array_merge($params->toArray(), $page->toArray()), 38 | ]; 39 | $httpResponse = $this->makeHttpRequest( 40 | "GET", 41 | '/v1/spaces/' . $this->spaceId . '/assets', 42 | options: $options 43 | ); 44 | 45 | return new AssetsResponse($httpResponse); 46 | } 47 | 48 | /** 49 | * Retrieving a single asset via the asset id. 50 | * @link https://www.storyblok.com/docs/api/management/core-resources/assets/retrieve-one-asset 51 | * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface 52 | */ 53 | public function get(string|int $assetId): AssetResponse 54 | { 55 | $httpResponse = $this->makeHttpRequest( 56 | "GET", 57 | '/v1/spaces/' . $this->spaceId . '/assets/' . $assetId 58 | ); 59 | return new AssetResponse($httpResponse); 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function buildPayload( 66 | string $filename, 67 | string|int|null $parent_id = null, 68 | ): array { 69 | $payload = [ 70 | 'filename' => $filename, 71 | //'size' => $width . 'x' . $height, 72 | 'validate_upload' => 1, 73 | 'parent_id' => $parent_id, 74 | ]; 75 | $size = getimagesize($filename); 76 | if ($size !== false) { 77 | $width = $size[0]; 78 | $height = $size[1]; 79 | $payload['size'] = $width . 'x' . $height; 80 | } 81 | 82 | return $payload; 83 | } 84 | 85 | public function upload(string $filename, string|int|null $parent_id = null): AssetUploadResponse 86 | { 87 | // =========== CREATE A SIGNED REQUEST 88 | $payload = $this->buildPayload($filename, $parent_id); 89 | 90 | $signedResponse = $this->makeRequest( 91 | "POST", 92 | '/v1/spaces/' . $this->spaceId . '/assets/', 93 | [ 'body' => $payload ], 94 | ); 95 | if (! $signedResponse->isOk()) { 96 | throw new \Exception( 97 | "Upload Asset, Signed Request call failed (Step 1) , " 98 | . $signedResponse->getResponseStatusCode() . " - " 99 | . $signedResponse->getErrorMessage(), 100 | ); 101 | } 102 | 103 | $signedResponseData = $signedResponse->data(); 104 | 105 | // =========== UPLOAD FILE for the SIGNED REQUEST 106 | $fields = $signedResponseData->get("fields"); 107 | $postFields = []; 108 | if ($fields instanceof StoryblokData) { 109 | $postFields = $fields->toArray(); 110 | } 111 | 112 | $postFields['file'] = fopen($filename, 'r'); 113 | $postUrl = $signedResponseData->getString('post_url'); 114 | 115 | /* 116 | $responseUpload = $this->makeHttpRequest( 117 | "POST", 118 | $postUrl, 119 | [ 120 | "body" => $postFields, 121 | ], 122 | 123 | ); 124 | */ 125 | 126 | $responseUpload = $this->managementClient->httpAssetClient()->request( 127 | "POST", 128 | $postUrl, 129 | [ 130 | "body" => $postFields, 131 | ], 132 | ); 133 | 134 | if (!($responseUpload->getStatusCode() >= 200 && $responseUpload->getStatusCode() < 300)) { 135 | //var_dump($responseUpload->getInfo()); 136 | throw new \Exception("Upload Asset, Upload call failed (Step 2) , " . $responseUpload->getStatusCode()); 137 | } 138 | 139 | $httpResponse = $this->makeHttpRequest( 140 | "GET", 141 | '/v1/spaces/' . 142 | $this->spaceId . 143 | '/assets/' . 144 | $signedResponseData->getString('id') . 145 | '/finish_upload' 146 | ); 147 | return new AssetUploadResponse($httpResponse); 148 | } 149 | 150 | /** 151 | * @param $assetId 152 | */ 153 | public function delete(string $assetId): AssetResponse 154 | { 155 | $httpResponse = $this->makeHttpRequest( 156 | "DELETE", 157 | '/v1/spaces/' . $this->spaceId . '/assets/' . $assetId 158 | ); 159 | return new AssetResponse($httpResponse); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Data/Component.php: -------------------------------------------------------------------------------- 1 | data = []; 20 | $this->data['name'] = $name; 21 | } 22 | 23 | /** 24 | * @param mixed[] $data 25 | * @throws StoryblokFormatException 26 | */ 27 | public static function make(array $data = []): self 28 | { 29 | $dataObject = new StoryblokData($data); 30 | if (!($dataObject->hasKey('name'))) { 31 | throw new StoryblokFormatException("Component is not valid, missing the name"); 32 | } 33 | 34 | $component = new Component( 35 | $dataObject->getString("name") 36 | ); 37 | $component->setData($dataObject->toArray()); 38 | // validate 39 | if (! $component->isValid()) { 40 | if ($dataObject->getString("name") === "") { 41 | throw new StoryblokFormatException("Component is not valid, missing the name"); 42 | } 43 | 44 | throw new StoryblokFormatException("Component <" . $dataObject->getString("name") . "> is not valid"); 45 | } 46 | 47 | return $component; 48 | 49 | } 50 | 51 | public function setName(string $name): void 52 | { 53 | $this->set('name', $name); 54 | } 55 | 56 | /** 57 | * Technical name used for component property in entries 58 | */ 59 | public function name(): string 60 | { 61 | return $this->getString('name'); 62 | } 63 | 64 | /** 65 | * Component name based on the technical name or display name 66 | */ 67 | public function realName(): string 68 | { 69 | return $this->getString('real_name'); 70 | } 71 | 72 | /** 73 | * Name that will be used in the editor interface 74 | */ 75 | public function displayName(): string 76 | { 77 | return $this->getString('display_name'); 78 | } 79 | 80 | /** 81 | * Name that will be used in the editor interface 82 | */ 83 | public function setDisplayName(string $displayName): void 84 | { 85 | $this->set('display_name', $displayName); 86 | } 87 | 88 | /** 89 | * URL to the preview image, if uploaded 90 | */ 91 | public function image(): string|null 92 | { 93 | return $this->getStringNullable('image'); 94 | } 95 | 96 | /** 97 | * URL to the preview image, if uploaded 98 | */ 99 | public function setImage(string $url): void 100 | { 101 | $this->set('image', $url); 102 | } 103 | 104 | /** 105 | * The field that is for preview in the interface (Preview Field) 106 | */ 107 | public function previewField(): string 108 | { 109 | return $this->getString('preview_field'); 110 | } 111 | 112 | /** 113 | * The field that is for preview in the interface (Preview Field) 114 | */ 115 | public function setPreviewField(string $value): void 116 | { 117 | $this->set('preview_field', $value); 118 | } 119 | 120 | /** 121 | * Creation date 122 | */ 123 | public function createdAt(string $format = self::DEFAULT_DATE_FORMAT): null|string 124 | { 125 | return $this->getFormattedDateTime('created_at', "", format: $format); 126 | } 127 | 128 | /** 129 | * Latest update date 130 | */ 131 | public function updatedAt(string $format = self::DEFAULT_DATE_FORMAT): null|string 132 | { 133 | return $this->getFormattedDateTime('updated_at', "", format: $format); 134 | } 135 | 136 | /** 137 | * The numeric ID in string format "12345678" 138 | */ 139 | public function id(): string 140 | { 141 | return $this->getString('id'); 142 | } 143 | 144 | /** 145 | * True if the component can be used as a Content Type 146 | */ 147 | public function isRoot(): bool 148 | { 149 | return $this->getBoolean('is_root'); 150 | } 151 | 152 | /** 153 | * If the component can be used as a Content Type 154 | */ 155 | public function setRoot(bool $isRoot = true): void 156 | { 157 | $this->set('is_root', $isRoot); 158 | } 159 | 160 | public function uuid(): string 161 | { 162 | return $this->getString('uuid'); 163 | } 164 | 165 | /** 166 | * @return mixed[] 167 | */ 168 | public function getSchema(): array 169 | { 170 | return $this->getArray('schema'); 171 | } 172 | 173 | /** 174 | * @param mixed[] $schema 175 | */ 176 | public function setSchema(array $schema): void 177 | { 178 | $this->set('schema', $schema); 179 | } 180 | 181 | /** 182 | * @param mixed[] $fieldAttributes 183 | */ 184 | public function setField(string $name, array $fieldAttributes): void 185 | { 186 | $this->set('schema.' . $name, $fieldAttributes); 187 | } 188 | 189 | /** 190 | * Validates if the component data contains all required fields and valid values 191 | */ 192 | public function isValid(): bool 193 | { 194 | return $this->hasKey('name'); 195 | } 196 | 197 | /** 198 | * Set tags for Component, from a `Tags` collection 199 | * @return $this 200 | */ 201 | public function setTags(Tags $tags): self 202 | { 203 | 204 | return $this->setTagsFromArray($tags->getTagsArray()); 205 | 206 | } 207 | 208 | /** 209 | * Set tags for Component, from a string of arrays like ["tag1", "tag2"] 210 | * @param string[] $tagsArray 211 | * @return $this 212 | */ 213 | public function setTagsFromArray(array $tagsArray): self 214 | { 215 | $this->set("internal_tag_ids", $tagsArray); 216 | return $this; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/QueryParameters/StoriesParams.php: -------------------------------------------------------------------------------- 1 | |string|null $excludingIds 13 | * @param array|string|null $byIds 14 | * @param array|string|null $byUuids 15 | * @param array|string|null $withTag 16 | * @param array|string|null $bySlugs 17 | * @param array|string|null $excludingSlugs 18 | * @param array|string|null $inWorkflowStages 19 | * @param array|string|null $byUuidsOrdered 20 | */ 21 | public function __construct( 22 | private readonly string|null $containComponent = null, 23 | private readonly string|null $textSearch = null, 24 | private readonly ?SortBy $sortBy = null, 25 | private readonly bool|null $pinned = null, 26 | private readonly array|string|null $excludingIds = null, 27 | private readonly array|string|null $byIds = null, 28 | private readonly array|string|null $byUuids = null, 29 | private readonly array|string|null $withTag = null, 30 | private readonly bool|null $folderOnly = null, 31 | private readonly bool|null $storyOnly = null, 32 | private readonly string|int|null $withParent = null, 33 | private readonly string|null $startsWith = null, 34 | private readonly bool|null $inTrash = null, 35 | private readonly string|null $search = null, 36 | private readonly string|int|null $inRelease = null, 37 | private readonly bool|null $isPublished = null, 38 | private readonly array|string|null $bySlugs = null, 39 | private readonly bool|null $mine = null, 40 | private readonly array|string|null $excludingSlugs = null, 41 | private readonly array|string|null $inWorkflowStages = null, 42 | private readonly array|string|null $byUuidsOrdered = null, 43 | private readonly string|null $withSlug = null, 44 | private readonly bool|null $withSummary = null, 45 | private readonly string|null $scheduledAtGreaterThan = null, 46 | private readonly string|null $scheduledAtLessThan = null, 47 | private readonly bool|null $favorite = null, 48 | private readonly string|null $referenceSearch = null, 49 | ) {} 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function toArray(): array 55 | { 56 | $array = []; 57 | if (null !== $this->containComponent) { 58 | $array['contain_component'] = $this->containComponent; 59 | } 60 | 61 | if (null !== $this->textSearch) { 62 | $array['text_search'] = $this->textSearch; 63 | } 64 | 65 | if ($this->sortBy instanceof SortBy) { 66 | $array['sort_by'] = $this->sortBy->toString(); 67 | } 68 | 69 | if ($this->pinned === true) { 70 | $array['pinned'] = "1"; 71 | } 72 | 73 | if (null !== $this->excludingIds) { 74 | if (is_array($this->excludingIds)) { 75 | $array['excluding_ids'] = implode(",", $this->excludingIds); 76 | } 77 | 78 | if (is_string($this->excludingIds)) { 79 | $array['excluding_ids'] = $this->excludingIds; 80 | } 81 | } 82 | 83 | if (null !== $this->byIds) { 84 | if (is_array($this->byIds)) { 85 | $array['by_ids'] = implode(",", $this->byIds); 86 | } 87 | 88 | if (is_string($this->byIds)) { 89 | $array['by_ids'] = $this->byIds; 90 | } 91 | } 92 | 93 | if (null !== $this->byUuids) { 94 | if (is_array($this->byUuids)) { 95 | $array['by_uuids'] = implode(",", $this->byUuids); 96 | } 97 | 98 | if (is_string($this->byUuids)) { 99 | $array['by_uuids'] = $this->byUuids; 100 | } 101 | } 102 | 103 | if (null !== $this->withTag) { 104 | if (is_array($this->withTag)) { 105 | $array['with_tag'] = implode(",", $this->withTag); 106 | } 107 | 108 | if (is_string($this->withTag)) { 109 | $array['with_tag'] = $this->withTag; 110 | } 111 | } 112 | 113 | if ($this->folderOnly === true) { 114 | $array['folder_only'] = "true"; 115 | } 116 | 117 | if ($this->storyOnly === true) { 118 | $array['story_only'] = "1"; 119 | } 120 | 121 | if (null !== $this->withParent) { 122 | $array['with_parent'] = $this->withParent; 123 | } 124 | 125 | if (null !== $this->startsWith) { 126 | $array['starts_with'] = $this->startsWith; 127 | } 128 | 129 | if ($this->inTrash === true) { 130 | $array['in_trash'] = "1"; 131 | } 132 | 133 | if (null !== $this->search) { 134 | $array['search'] = $this->search; 135 | } 136 | 137 | if (null !== $this->inRelease) { 138 | $array['in_release'] = $this->inRelease; 139 | } 140 | 141 | if ($this->isPublished === true) { 142 | $array['is_published'] = "1"; 143 | } 144 | 145 | if (null !== $this->bySlugs) { 146 | if (is_array($this->bySlugs)) { 147 | $array['by_slugs'] = implode(",", $this->bySlugs); 148 | } 149 | 150 | if (is_string($this->bySlugs)) { 151 | $array['by_slugs'] = $this->bySlugs; 152 | } 153 | } 154 | 155 | if ($this->mine === true) { 156 | $array['mine'] = "true"; 157 | } 158 | 159 | if (null !== $this->excludingSlugs) { 160 | if (is_array($this->excludingSlugs)) { 161 | $array['excluding_slugs'] = implode(",", $this->excludingSlugs); 162 | } 163 | 164 | if (is_string($this->excludingSlugs)) { 165 | $array['excluding_slugs'] = $this->excludingSlugs; 166 | } 167 | } 168 | 169 | if (null !== $this->inWorkflowStages) { 170 | if (is_array($this->inWorkflowStages)) { 171 | $array['in_workflow_stages'] = implode(",", $this->inWorkflowStages); 172 | } 173 | 174 | if (is_string($this->inWorkflowStages)) { 175 | $array['in_workflow_stages'] = $this->inWorkflowStages; 176 | } 177 | } 178 | 179 | if (null !== $this->byUuidsOrdered) { 180 | if (is_array($this->byUuidsOrdered)) { 181 | $array['by_uuids_ordered'] = implode(",", $this->byUuidsOrdered); 182 | } 183 | 184 | if (is_string($this->byUuidsOrdered)) { 185 | $array['by_uuids_ordered'] = $this->byUuidsOrdered; 186 | } 187 | } 188 | 189 | if (null !== $this->withSlug) { 190 | $array['with_slug'] = $this->withSlug; 191 | } 192 | 193 | if ($this->withSummary === true) { 194 | $array['with_summary'] = "1"; 195 | } 196 | 197 | if (null !== $this->scheduledAtGreaterThan) { 198 | $array['scheduled_at_gt'] = $this->scheduledAtGreaterThan; 199 | } 200 | 201 | if (null !== $this->scheduledAtLessThan) { 202 | $array['scheduled_at_lt'] = $this->scheduledAtLessThan; 203 | } 204 | 205 | if ($this->favorite === true) { 206 | $array['favorite'] = "1"; 207 | } 208 | 209 | if (null !== $this->referenceSearch) { 210 | $array['reference_search'] = $this->referenceSearch; 211 | } 212 | 213 | return $array; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Endpoints/StoryBulkApi.php: -------------------------------------------------------------------------------- 1 | api = new StoryApi($managementClient, $spaceId, $logger); 49 | } 50 | 51 | /** 52 | * Retrieves all stories using pagination 53 | * 54 | * @param int $itemsPerPage Number of items to retrieve per page 55 | * @return \Generator 56 | * @throws StoryblokApiException 57 | */ 58 | public function all( 59 | ?StoriesParams $params = null, 60 | ?QueryFilters $filters = null, 61 | int $itemsPerPage = self::DEFAULT_ITEMS_PER_PAGE, 62 | ): \Generator { 63 | $totalPages = null; 64 | $retryCount = 0; 65 | $page = new PaginationParams(self::DEFAULT_PAGE, $itemsPerPage); 66 | 67 | do { 68 | try { 69 | $response = $this->api->page( 70 | params: $params, 71 | queryFilters: $filters, 72 | page: $page, 73 | ); 74 | 75 | if ($response->isOk()) { 76 | $totalPages = $this->handleSuccessfulResponse( 77 | $response, 78 | $totalPages, 79 | $itemsPerPage, 80 | ); 81 | yield from $this->getStoriesFromResponse($response); 82 | $page->incrementPage(); 83 | $retryCount = 0; 84 | } else { 85 | $this->handleErrorResponse($response, $retryCount); 86 | ++$retryCount; 87 | } 88 | } catch (\Exception $e) { 89 | $this->logger->error("Error fetching stories", [ 90 | "error" => $e->getMessage(), 91 | "page" => $page->page(), 92 | ]); 93 | throw new StoryblokApiException( 94 | "Failed to fetch stories: " . $e->getMessage(), 95 | 0, 96 | $e, 97 | ); 98 | } 99 | } while ($totalPages === null || $page->page() <= $totalPages); 100 | } 101 | 102 | /** 103 | * Creates multiple stories with rate limit handling and retries 104 | * 105 | * @param Story[] $stories Array of stories to create 106 | * @return \Generator Generated stories 107 | * @throws StoryblokApiException 108 | */ 109 | public function createStories(array $stories): \Generator 110 | { 111 | $retryCount = 0; 112 | 113 | foreach ($stories as $storyData) { 114 | while (true) { 115 | try { 116 | $response = $this->api->create($storyData); 117 | $this->logger->warning( 118 | "Story created " . $response->getResponseStatusCode(), 119 | ); 120 | yield $response->data(); 121 | $retryCount = 0; 122 | break; 123 | } catch (\Exception $e) { 124 | if ($e->getCode() === self::RATE_LIMIT_STATUS_CODE) { 125 | if ($retryCount >= self::MAX_RETRIES) { 126 | $this->logger->error( 127 | "Max retries reached while creating story", 128 | [ 129 | "story_name" => $storyData->name(), 130 | ], 131 | ); 132 | throw new StoryblokApiException( 133 | "Rate limit exceeded maximum retries", 134 | self::RATE_LIMIT_STATUS_CODE, 135 | ); 136 | } 137 | 138 | $this->logger->warning( 139 | "Rate limit reached while creating story, retrying...", 140 | [ 141 | "retry_count" => $retryCount + 1, 142 | "max_retries" => self::MAX_RETRIES, 143 | "story_name" => $storyData->name(), 144 | ], 145 | ); 146 | 147 | $this->handleRateLimit(); 148 | ++$retryCount; 149 | continue; 150 | } 151 | 152 | throw $e; 153 | } 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * Handles successful API response 160 | */ 161 | private function handleSuccessfulResponse( 162 | StoryblokResponseInterface $response, 163 | ?int $totalPages, 164 | int $itemsPerPage, 165 | ): int { 166 | if ($totalPages === null) { 167 | $totalPages = (int) ceil($response->total() / $itemsPerPage); 168 | $this->logger->info("Total stories found: " . $response->total()); 169 | } 170 | 171 | return $totalPages; 172 | } 173 | 174 | /** 175 | * Handles error responses from the API 176 | * 177 | * @throws StoryblokApiException 178 | */ 179 | private function handleErrorResponse( 180 | StoryblokResponseInterface $response, 181 | int $retryCount, 182 | ): void { 183 | if ( 184 | $response->getResponseStatusCode() === self::RATE_LIMIT_STATUS_CODE 185 | ) { 186 | if ($retryCount < self::MAX_RETRIES) { 187 | $this->handleRateLimit(); 188 | } else { 189 | throw new StoryblokApiException( 190 | "Rate limit exceeded maximum retries", 191 | ); 192 | } 193 | } else { 194 | $this->logger->error("API error", [ 195 | "status" => $response->getResponseStatusCode(), 196 | "message" => $response->getErrorMessage(), 197 | ]); 198 | throw new StoryblokApiException($response->getErrorMessage()); 199 | } 200 | } 201 | 202 | /** 203 | * Handles rate limiting by implementing a delay 204 | */ 205 | protected function handleRateLimit(): void 206 | { 207 | $this->logger->warning("Rate limit reached, waiting before retry..."); 208 | sleep(self::RETRY_DELAY); 209 | } 210 | 211 | /** 212 | * Extracts stories from the API response 213 | * 214 | * @return \Generator 215 | */ 216 | private function getStoriesFromResponse( 217 | StoryblokResponseInterface $response, 218 | ): \Generator { 219 | /** @var Stories $stories */ 220 | $stories = $response->data(); 221 | foreach ($stories as $story) { 222 | /** @var Story $story */ 223 | yield $story; 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Endpoints/StoryApi.php: -------------------------------------------------------------------------------- 1 | validatePaginationParams($page); 64 | 65 | $options = [ 66 | "query" => array_merge( 67 | $params->toArray(), 68 | $queryFilters->toArray(), 69 | $page->toArray(), 70 | ), 71 | ]; 72 | 73 | $httpResponse = $this->makeHttpRequest( 74 | "GET", 75 | $this->buildStoriesEndpoint(), 76 | options: $options, 77 | ); 78 | return new StoriesResponse($httpResponse); 79 | } 80 | 81 | /** 82 | * Retrieves a specific story by ID 83 | * 84 | * @throws StoryblokApiException 85 | */ 86 | public function get(string $storyId): StoryResponse 87 | { 88 | $this->validateStoryId($storyId); 89 | 90 | $httpResponse = $this->makeHttpRequest( 91 | "GET", 92 | $this->buildStoryEndpoint($storyId), 93 | ); 94 | return new StoryResponse($httpResponse); 95 | } 96 | 97 | /** 98 | * Creates a new story 99 | * @param bool $publish set as true if you want to publish the story immediatly 100 | * @param int $releaseId set the release id if you want to create the story in a specific release 101 | */ 102 | public function create( 103 | Story $storyData, 104 | bool $publish = false, 105 | int $releaseId = 0, 106 | ): StoryResponse { 107 | $this->validateStoryData($storyData); 108 | 109 | if (!$storyData->hasKey("content")) { 110 | $storyData->setContent( 111 | new StoryComponent($storyData->defaultContentType()), 112 | ); 113 | } 114 | 115 | $payload = ["story" => $storyData->toArray()]; 116 | if ($publish) { 117 | $payload["publish"] = 1; 118 | } 119 | 120 | if ($releaseId > 0) { 121 | $payload["release_id"] = $releaseId; 122 | } 123 | 124 | $httpResponse = $this->makeHttpRequest( 125 | "POST", 126 | $this->buildStoriesEndpoint(), 127 | [ 128 | "body" => json_encode($payload), 129 | ], 130 | ); 131 | 132 | return new StoryResponse($httpResponse); 133 | } 134 | 135 | /** 136 | * Updates an existing story 137 | * 138 | * @throws InvalidStoryDataException 139 | */ 140 | public function update(string $storyId, Story $storyData): StoryResponse 141 | { 142 | $this->validateStoryId($storyId); 143 | //$this->validateStoryData($storyData); 144 | 145 | $httpResponse = $this->makeHttpRequest( 146 | "PUT", 147 | $this->buildStoryEndpoint($storyId), 148 | [ 149 | "body" => json_encode(["story" => $storyData->toArray()]), 150 | ], 151 | ); 152 | return new StoryResponse($httpResponse); 153 | } 154 | 155 | public function publish( 156 | string $storyId, 157 | int|string|null $release_id = null, 158 | string|null $language = null, 159 | ): StoryResponse { 160 | $this->validateStoryId($storyId); 161 | //$this->validateStoryData($storyData); 162 | 163 | $queryParams = []; 164 | if (null !== $release_id) { 165 | $queryParams["release_id"] = $release_id; 166 | } 167 | 168 | if (null !== $language) { 169 | $queryParams["lang"] = $language; 170 | } 171 | 172 | $httpResponse = $this->makeHttpRequest( 173 | "GET", 174 | sprintf("%s/%s/publish", $this->buildStoriesEndpoint(), $storyId), 175 | [ 176 | "query" => $queryParams, 177 | ], 178 | ); 179 | return new StoryResponse($httpResponse); 180 | } 181 | 182 | public function unpublish( 183 | string $storyId, 184 | string|null $language = null, 185 | ): StoryResponse { 186 | $this->validateStoryId($storyId); 187 | //$this->validateStoryData($storyData); 188 | 189 | $queryParams = []; 190 | 191 | if (null !== $language) { 192 | $queryParams["lang"] = $language; 193 | } 194 | 195 | $httpResponse = $this->makeHttpRequest( 196 | "GET", 197 | sprintf("%s/%s/unpublish", $this->buildStoriesEndpoint(), $storyId), 198 | [ 199 | "query" => $queryParams, 200 | ], 201 | ); 202 | return new StoryResponse($httpResponse); 203 | } 204 | 205 | /** 206 | * Validates pagination parameters 207 | * 208 | * @throws \InvalidArgumentException 209 | */ 210 | private function validatePaginationParams(PaginationParams $page): void 211 | { 212 | if ($page->page() < 1) { 213 | throw new \InvalidArgumentException( 214 | "Page number must be greater than 0", 215 | ); 216 | } 217 | 218 | if ($page->perPage() < 1) { 219 | throw new \InvalidArgumentException( 220 | "Items per page must be greater than 0", 221 | ); 222 | } 223 | } 224 | 225 | /** 226 | * Validates story ID 227 | * 228 | * @throws \InvalidArgumentException 229 | */ 230 | private function validateStoryId(string $storyId): void 231 | { 232 | if ($storyId === "" || $storyId === "0") { 233 | throw new \InvalidArgumentException("Story ID cannot be empty"); 234 | } 235 | } 236 | 237 | /** 238 | * Validates story data 239 | * 240 | * @throws InvalidStoryDataException 241 | */ 242 | private function validateStoryData(Story $storyData): void 243 | { 244 | if (!$storyData->isValid()) { 245 | throw new InvalidStoryDataException("Invalid story data provided"); 246 | } 247 | } 248 | 249 | /** 250 | * Builds the base endpoint for stories 251 | */ 252 | private function buildStoriesEndpoint(): string 253 | { 254 | return sprintf("/v1/spaces/%s/stories", $this->spaceId); 255 | } 256 | 257 | /** 258 | * Builds the endpoint for a specific story 259 | */ 260 | private function buildStoryEndpoint(string $storyId): string 261 | { 262 | return sprintf("%s/%s", $this->buildStoriesEndpoint(), $storyId); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/Data/BaseData.php: -------------------------------------------------------------------------------- 1 | 21 | * @implements Iterator 22 | */ 23 | abstract class BaseData implements 24 | StoryblokDataInterface, 25 | Iterator, 26 | ArrayAccess, 27 | Countable 28 | { 29 | use IterableDataTrait; 30 | 31 | /** 32 | * @var mixed[] $data 33 | */ 34 | protected array $data = []; 35 | 36 | /** 37 | * @return mixed[] 38 | */ 39 | public function toArray(): array 40 | { 41 | return $this->data; 42 | } 43 | 44 | /** 45 | * Set the internal data from an array 46 | * 47 | * @param mixed[] $data The underlying data in array form. 48 | */ 49 | public function setData(array $data): void 50 | { 51 | $this->data = $data; 52 | } 53 | 54 | public function getInt( 55 | int|string $key, 56 | int|null $defaultValue = null, 57 | string $charNestedKey = ".", 58 | ): int|null { 59 | $returnValue = $this->get($key, null, $charNestedKey); 60 | 61 | if (is_scalar($returnValue)) { 62 | return intval($returnValue); 63 | } 64 | 65 | return $defaultValue; 66 | } 67 | 68 | public function getBoolean( 69 | int|string $key, 70 | bool $defaultValue = false, 71 | string $charNestedKey = ".", 72 | ): bool { 73 | $returnValue = $this->get($key, $defaultValue, $charNestedKey); 74 | 75 | if (is_scalar($returnValue)) { 76 | return boolval($returnValue); 77 | } 78 | 79 | return $defaultValue; 80 | } 81 | 82 | /** 83 | * @param mixed[] $defaultValue 84 | * @return mixed[] 85 | */ 86 | public function getArray( 87 | int|string $key, 88 | array $defaultValue = [], 89 | string $charNestedKey = ".", 90 | ): array { 91 | $returnValue = $this->get($key, $defaultValue, $charNestedKey); 92 | 93 | if (is_scalar($returnValue)) { 94 | return [strval($returnValue)]; 95 | } 96 | 97 | if ($returnValue instanceof StoryblokData) { 98 | return $returnValue->toArray(); 99 | } 100 | 101 | return $defaultValue; 102 | } 103 | 104 | public function getFormattedDateTime( 105 | int|string $key, 106 | string $defaultValue = "", 107 | string $charNestedKey = ".", 108 | string $format = "Y-m-d H:i:s", 109 | ): string|null { 110 | $value = $this->getString($key, "", $charNestedKey); 111 | 112 | if ($value === "") { 113 | return ""; 114 | } 115 | 116 | try { 117 | $date = new \DateTimeImmutable($value); 118 | } catch (\DateMalformedStringException) { 119 | return $defaultValue; 120 | } 121 | 122 | return $date->format($format); 123 | } 124 | 125 | /** 126 | * Sets a value for a specific key in the data. 127 | * Supports dot notation for nested keys. 128 | * 129 | * @param int|string $key The key to set the value for. Can use dot notation for nested keys. 130 | * @param mixed $value The value to set. 131 | * @param string $charNestedKey The character used for separating nested keys (default: "."). 132 | * @return $this The current instance for method chaining. 133 | */ 134 | public function set( 135 | int|string $key, 136 | mixed $value, 137 | string $charNestedKey = ".", 138 | ): self { 139 | if (is_string($key)) { 140 | $array = &$this->data; 141 | if ($charNestedKey === "") { 142 | $charNestedKey = "."; 143 | } 144 | 145 | $keys = explode($charNestedKey, $key); 146 | foreach ($keys as $i => $key) { 147 | if (count($keys) === 1) { 148 | break; 149 | } 150 | 151 | unset($keys[$i]); 152 | 153 | if (!isset($array[$key]) || !is_array($array[$key])) { 154 | $array[$key] = []; 155 | } 156 | 157 | $array = &$array[$key]; 158 | } 159 | 160 | $array[array_shift($keys)] = $value; 161 | return $this; 162 | } 163 | 164 | $this->data[$key] = $value; 165 | return $this; 166 | } 167 | 168 | /** 169 | * Returns data in the desired format. 170 | * Can be raw or converted to StoryblokData. 171 | * 172 | * @param mixed $value The value to process. 173 | * @param bool $raw Whether to return raw data or cast it into StoryblokData if applicable. 174 | * @return mixed The processed value. 175 | */ 176 | protected function returnData(mixed $value, bool $raw = false): mixed 177 | { 178 | if (is_null($value)) { 179 | return null; 180 | } 181 | 182 | if (is_scalar($value)) { 183 | return $value; 184 | } 185 | 186 | if ($raw) { 187 | return $value; 188 | } 189 | 190 | if (is_array($value)) { 191 | return new StoryblokData($value); 192 | } 193 | 194 | if ($value instanceof StoryblokData) { 195 | return $value; 196 | } 197 | 198 | return new StoryblokData([]); 199 | } 200 | 201 | /** 202 | * Returns the class name of the current data class. 203 | * This is useful when you extend the StoryblokData 204 | * and you want to cast the items returned during 205 | * the iteration (loops like foreach) 206 | * @return string The fully qualified class name. 207 | */ 208 | public function getDataClass(): string 209 | { 210 | return StoryblokData::class; 211 | } 212 | 213 | /** 214 | * Counts the number of top-level elements in the data. 215 | * 216 | * @return int The number of elements in the data. 217 | */ 218 | public function count(): int 219 | { 220 | return count($this->data); 221 | } 222 | 223 | public function has(mixed $value): bool 224 | { 225 | return in_array($value, $this->values()->toArray()); 226 | } 227 | 228 | public function hasKey(string|int $key): bool 229 | { 230 | /** @var array $keys */ 231 | $keys = $this->keys(); 232 | return in_array($key, $keys); 233 | } 234 | 235 | /** 236 | * @return StoryblokData object that contains the key/value pairs for each index in the array 237 | */ 238 | public function values(): self 239 | { 240 | $pairs = $this->data; 241 | return StoryblokData::make($pairs); 242 | } 243 | 244 | /** 245 | * Returns a new array [] or a new StoryblokData object that contains the keys 246 | * for each index in the Block object 247 | * It returns StoryblokData or [] depending on $returnArrClass value 248 | * 249 | * @param bool $returnBlockClass true if you need StoryblokData object 250 | * @return int|string|array|self 251 | */ 252 | public function keys(bool $returnBlockClass = false): int|string|array|self 253 | { 254 | if ($returnBlockClass) { 255 | return StoryblokData::make(array_keys($this->data)); 256 | } 257 | 258 | return array_keys($this->data); 259 | } 260 | 261 | public function toJson(): string|false 262 | { 263 | return json_encode($this->data, JSON_PRETTY_PRINT); 264 | } 265 | 266 | public function dump(): void 267 | { 268 | echo $this->toJson(); 269 | } 270 | 271 | /** 272 | * Retrieves a value from the data by key. Supports dot notation for nested keys. 273 | * 274 | * @param int|string $key The key to retrieve the value for. Can use dot notation for nested keys. 275 | * @param mixed|null $defaultValue The default value to return if the key does not exist. 276 | * @param string $charNestedKey The character used for separating nested keys (default: "."). 277 | * @param bool $raw Whether to return raw data or cast it into StoryblokData if applicable. 278 | * @return mixed The value associated with the key, or the default value if the key does not exist. 279 | */ 280 | public function get( 281 | int|string $key, 282 | mixed $defaultValue = null, 283 | string $charNestedKey = ".", 284 | bool $raw = false, 285 | ): mixed { 286 | if (is_string($key)) { 287 | if ($charNestedKey === "") { 288 | $charNestedKey = "."; 289 | } 290 | 291 | $keyString = strval($key); 292 | if (str_contains($keyString, $charNestedKey)) { 293 | $nestedValue = $this->data; 294 | foreach (explode($charNestedKey, $keyString) as $nestedKey) { 295 | if ( 296 | is_array($nestedValue) && 297 | array_key_exists($nestedKey, $nestedValue) 298 | ) { 299 | $nestedValue = $nestedValue[$nestedKey]; 300 | } elseif ($nestedValue instanceof StoryblokData) { 301 | $nestedValue = $nestedValue->get($nestedKey); 302 | } else { 303 | return $defaultValue; 304 | } 305 | } 306 | 307 | return $this->returnData($nestedValue, $raw); 308 | } 309 | 310 | if (!array_key_exists($key, $this->data)) { 311 | return $defaultValue; 312 | } 313 | } 314 | 315 | if (is_numeric($key) && !array_key_exists($key, $this->data)) { 316 | return $defaultValue; 317 | } 318 | 319 | return $this->returnData($this->data[$key], $raw) ?? $defaultValue; 320 | } 321 | 322 | public function getString( 323 | int|string $key, 324 | string $defaultValue = "", 325 | string $charNestedKey = ".", 326 | ): string { 327 | $returnValue = $this->get($key, "", $charNestedKey); 328 | 329 | if (is_scalar($returnValue)) { 330 | return strval($returnValue); 331 | } 332 | 333 | return $defaultValue; 334 | } 335 | 336 | public function getStringNullable( 337 | int|string $key, 338 | string|null $defaultValue = null, 339 | string $charNestedKey = ".", 340 | ): string|null { 341 | $returnValue = $this->get($key, $defaultValue, $charNestedKey); 342 | 343 | if (is_scalar($returnValue)) { 344 | return strval($returnValue); 345 | } 346 | 347 | return $defaultValue; 348 | } 349 | } 350 | --------------------------------------------------------------------------------