From b4ff6666c381efbd7cc8aabf7342806d3510b60a Mon Sep 17 00:00:00 2001 From: Marc Duiker Date: Fri, 21 Nov 2025 13:56:33 +0000 Subject: [PATCH 1/3] Remove php submodule Signed-off-by: Marc Duiker --- .gitmodules | 3 --- hugo.yaml | 2 +- sdkdocs/php | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) delete mode 160000 sdkdocs/php diff --git a/.gitmodules b/.gitmodules index cb2249af216..a41a3e9781b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "sdkdocs/python"] path = sdkdocs/python url = https://github.com/dapr/python-sdk.git -[submodule "sdkdocs/php"] - path = sdkdocs/php - url = https://github.com/dapr/php-sdk.git [submodule "translations/docs-zh"] path = translations/docs-zh url = https://github.com/dapr/docs-zh.git diff --git a/hugo.yaml b/hugo.yaml index 31b591c9817..722e1fcf019 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -281,7 +281,7 @@ module: - source: sdkdocs/python/daprdocs/content/en/python-sdk-contributing target: content/contributing/sdk-contrib/ lang: en - - source: sdkdocs/php/daprdocs/content/en/php-sdk-docs + - source: sdkdocs/php/content/en/php-sdk-docs target: content/developing-applications/sdks/php lang: en - source: sdkdocs/dotnet/content/en/dotnet-sdk-docs diff --git a/sdkdocs/php b/sdkdocs/php deleted file mode 160000 index 82c7283a73c..00000000000 --- a/sdkdocs/php +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 82c7283a73cf96e27b93383531094cf82cd2c6c4 From 97d27cc3d8d26eb2f6b39c4247fd7d20fa3b3f0d Mon Sep 17 00:00:00 2001 From: Marc Duiker Date: Fri, 21 Nov 2025 14:07:01 +0000 Subject: [PATCH 2/3] Add PHP SDK docs Signed-off-by: Marc Duiker --- sdkdocs/php/content/en/php-sdk-docs/_index.md | 116 +++++++ .../en/php-sdk-docs/php-actors/_index.md | 164 ++++++++++ .../php-actors/php-actor-reference.md | 238 +++++++++++++++ .../content/en/php-sdk-docs/php-app/_index.md | 90 ++++++ .../php-sdk-docs/php-app/php-unit-testing.md | 285 ++++++++++++++++++ .../en/php-sdk-docs/php-pubsub/_index.md | 63 ++++ .../en/php-sdk-docs/php-serialization.md | 55 ++++ .../en/php-sdk-docs/php-state/_index.md | 99 ++++++ sdkdocs/php/readme.md | 0 9 files changed, 1110 insertions(+) create mode 100644 sdkdocs/php/content/en/php-sdk-docs/_index.md create mode 100644 sdkdocs/php/content/en/php-sdk-docs/php-actors/_index.md create mode 100644 sdkdocs/php/content/en/php-sdk-docs/php-actors/php-actor-reference.md create mode 100644 sdkdocs/php/content/en/php-sdk-docs/php-app/_index.md create mode 100644 sdkdocs/php/content/en/php-sdk-docs/php-app/php-unit-testing.md create mode 100644 sdkdocs/php/content/en/php-sdk-docs/php-pubsub/_index.md create mode 100644 sdkdocs/php/content/en/php-sdk-docs/php-serialization.md create mode 100644 sdkdocs/php/content/en/php-sdk-docs/php-state/_index.md create mode 100644 sdkdocs/php/readme.md diff --git a/sdkdocs/php/content/en/php-sdk-docs/_index.md b/sdkdocs/php/content/en/php-sdk-docs/_index.md new file mode 100644 index 00000000000..845f5360518 --- /dev/null +++ b/sdkdocs/php/content/en/php-sdk-docs/_index.md @@ -0,0 +1,116 @@ +--- +type: docs +title: "Dapr PHP SDK" +linkTitle: "PHP" +weight: 1000 +description: PHP SDK packages for developing Dapr applications +no_list: true +cascade: + github_repo: https://github.com/dapr/php-sdk + github_subdir: daprdocs/content/en/php-sdk-docs + path_base_for_github_subdir: content/en/developing-applications/sdks/php/ + github_branch: main +--- + +Dapr offers an SDK to help with the development of PHP applications. Using it, you can create PHP clients, servers, and virtual actors with Dapr. + +## Setting up + +### Prerequisites + +- [Composer](https://getcomposer.org/) +- [PHP 8](https://www.php.net/) + +### Optional Prerequisites + +- [Docker](https://www.docker.com/) +- [xdebug](http://xdebug.org/) -- for debugging + +## Initialize your project + +In a directory where you want to create your service, run `composer init` and answer the questions. +Install with `composer require dapr/php-sdk` and any other dependencies you may wish to use. + +## Configure your service + +Create a config.php, copying the contents below: + +```php + LogLevel::WARNING, + + // Generate a new proxy on each request - recommended for development + 'dapr.actors.proxy.generation' => ProxyFactory::GENERATED, + + // put any subscriptions here + 'dapr.subscriptions' => [], + + // if this service will be hosting any actors, add them here + 'dapr.actors' => [], + + // if this service will be hosting any actors, configure how long until dapr should consider an actor idle + 'dapr.actors.idle_timeout' => null, + + // if this service will be hosting any actors, configure how often dapr will check for idle actors + 'dapr.actors.scan_interval' => null, + + // if this service will be hosting any actors, configure how long dapr will wait for an actor to finish during drains + 'dapr.actors.drain_timeout' => null, + + // if this service will be hosting any actors, configure if dapr should wait for an actor to finish + 'dapr.actors.drain_enabled' => null, + + // you shouldn't have to change this, but the setting is here if you need to + 'dapr.port' => env('DAPR_HTTP_PORT', '3500'), + + // add any custom serialization routines here + 'dapr.serializers.custom' => [], + + // add any custom deserialization routines here + 'dapr.deserializers.custom' => [], + + // the following has no effect, as it is the default middlewares and processed in order specified + 'dapr.http.middleware.request' => [get(Tracing::class)], + 'dapr.http.middleware.response' => [get(ApplicationJson::class), get(Tracing::class)], +]; +``` + +## Create your service + +Create `index.php` and put the following contents: + +```php + $builder->addDefinitions(__DIR__ . '/config.php')); +$app->get('/hello/{name}', function(string $name) { + return ['hello' => $name]; +}); +$app->start(); +``` + +## Try it out + +Initialize dapr with `dapr init` and then start the project with `dapr run -a dev -p 3000 -- php -S 0.0.0.0:3000`. + +You can now open a web browser and point it to [http://localhost:3000/hello/world](http://localhost:3000/hello/world) +replacing `world` with your name, a pet's name, or whatever you want. + +Congratulations, you've created your first Dapr service! I'm excited to see what you'll do with it! + +## More Information + +- [Packagist](https://packagist.org/packages/dapr/php-sdk) +- [Dapr SDK serialization]({{% ref sdk-serialization.md %}}) diff --git a/sdkdocs/php/content/en/php-sdk-docs/php-actors/_index.md b/sdkdocs/php/content/en/php-sdk-docs/php-actors/_index.md new file mode 100644 index 00000000000..bb6d3cc3ce6 --- /dev/null +++ b/sdkdocs/php/content/en/php-sdk-docs/php-actors/_index.md @@ -0,0 +1,164 @@ +--- +type: docs +title: "Virtual Actors" +linkTitle: "Actors" +weight: 1000 +description: How to build actors +no_list: true +--- + +If you're new to the actor pattern, the best place to learn about the actor pattern is in +the [Actor Overview.]({{% ref actors-overview.md %}}) + +In the PHP SDK, there are two sides to an actor, the Client, and the Actor (aka, the Runtime). As a client of an actor, +you'll interact with a remote actor via the `ActorProxy` class. This class generates a proxy class on-the-fly using one +of several configured strategies. + +When writing an actor, state can be managed for you. You can hook into the actor lifecycle, and define reminders and +timers. This gives you considerable power for handling all types of problems that the actor pattern is suited for. + +## The Actor Proxy + +Whenever you want to communicate with an actor, you'll need to get a proxy object to do so. The proxy is responsible for +serializing your request, deserializing the response, and returning it to you, all while obeying the contract defined by +the specified interface. + +In order to create the proxy, you'll first need an interface to define how and what you send and receive from an actor. +For example, if you want to communicate with a counting actor that solely keeps track of counts, you might define the +interface as follows: + +```php +run(function(\Dapr\Actors\ActorProxy $actorProxy) { + $actor = $actorProxy->get(ICount::class, 'actor-id'); + $actor->increment(10); +}); +``` + +## Writing Actors + +To create an actor, you need to implement the interface you defined earlier and also add the `DaprType` attribute. All +actors *must* implement `IActor`, however there's an `Actor` base class that implements the boilerplate making your +implementation much simpler. + +Here's the counter actor: + +```php +state->count += $amount; + } + + function get_count(): int { + return $this->state->count; + } +} +``` + +The most important bit is the constructor. It takes at least one argument with the name of `id` which is the id of the +actor. Any additional arguments are injected by the DI container, including any `ActorState` you want to use. + +### Actor Lifecycle + +An actor is instantiated via the constructor on every request targeting that actor type. You can use it to calculate +ephemeral state or handle any kind of request-specific startup you require, such as setting up other clients or +connections. + +After the actor is instantiated, the `on_activation()` method may be called. The `on_activation()` method is called any +time the actor "wakes up" or when it is created for the first time. It is not called on every request. + +Next, the actor method is called. This may be from a timer, reminder, or from a client. You may perform any work that +needs to be done and/or throw an exception. + +Finally, the result of the work is returned to the caller. After some time (depending on how you've configured the +service), the actor will be deactivated and `on_deactivation()` will be called. This may not be called if the host dies, +daprd crashes, or some other error occurs which prevents it from being called successfully. + +## Actor State + +Actor state is a "Plain Old PHP Object" (POPO) that extends `ActorState`. The `ActorState` base class provides a couple +of useful methods. Here's an example implementation: + +```php +}} + +{{% tab header="Production" %}} + +If you want to take advantage of pre-compiled dependency injection, you need to use a factory: + +```php + fn() => [Counter::class], +]; +``` + +All that is required to start the app: + +```php + $builder->addDefinitions('config.php')->enableCompilation(__DIR__) +); +$app->start(); +``` + +{{% /tab %}} +{{% tab header="Development" %}} + +```php + [Counter::class] +]; +``` + +All that is required to start the app: + +```php + $builder->addDefinitions('config.php')); +$app->start(); +``` + +{{% /tab %}} +{{< /tabpane >}} diff --git a/sdkdocs/php/content/en/php-sdk-docs/php-actors/php-actor-reference.md b/sdkdocs/php/content/en/php-sdk-docs/php-actors/php-actor-reference.md new file mode 100644 index 00000000000..0253934e3da --- /dev/null +++ b/sdkdocs/php/content/en/php-sdk-docs/php-actors/php-actor-reference.md @@ -0,0 +1,238 @@ +--- +type: docs +title: "Production Reference: Actors" +linkTitle: "Production Reference" +weight: 1000 +description: Running PHP actors in production +no_list: true +--- + +## Proxy modes + +There are four different modes actor proxies are handled. Each mode presents different trade-offs that you'll need to +weigh during development and in production. + +```php +}} +{{% tab header="GENERATED" %}} + +This is the default mode. In this mode, a class is generated and `eval`'d on every request. It's mostly for development +and shouldn't be used in production. + +{{% /tab %}} +{{% tab header="GENERATED_CACHED" %}} + +This is the same as `ProxyModes::GENERATED` except the class is stored in a tmp file so it doesn't need to be +regenerated on every request. It doesn't know when to update the cached class, so using it in development is discouraged +but is offered for when manually generating the files isn't possible. + +{{% /tab %}} +{{% tab header="ONLY_EXISTING" %}} + +In this mode, an exception is thrown if the proxy class doesn't exist. This is useful for when you don't want to +generate code in production. You'll have to make sure the class is generated and pre-/autoloaded. + +### Generating proxies + +You can create a composer script to generate proxies on demand to take advantage of the `ONLY_EXISTING` mode. + +Create a `ProxyCompiler.php` + +```php +run(function(\DI\FactoryInterface $factory) use ($interface) { + return \Dapr\Actors\Generators\FileGenerator::generate($interface, $factory); + }); + $reflection = new ReflectionClass($interface); + $dapr_type = $reflection->getAttributes(\Dapr\Actors\Attributes\DaprType::class)[0]->newInstance()->type; + $filename = 'dapr_proxy_'.$dapr_type.'.php'; + file_put_contents(self::PROXY_LOCATION.$filename, $output); + echo "Compiled: $interface"; + } + } catch (Exception $ex) { + echo "Failed to generate proxy for $interface\n{$ex->getMessage()} on line {$ex->getLine()} in {$ex->getFile()}\n"; + } + } +} +``` + +Then add a psr-4 autoloader for the generated proxies and a script in `composer.json`: + +```json +{ + "autoload": { + "psr-4": { + "Dapr\\Proxies\\": "path/to/proxies" + } + }, + "scripts": { + "compile-proxies": "ProxyCompiler::compile" + } +} +``` + +And finally, configure dapr to only use the generated proxies: + +```php + ProxyFactory::ONLY_EXISTING, +]; +``` + +{{% /tab %}} +{{% tab header="DYNAMIC" %}} + +In this mode, the proxy satisfies the interface contract, however, it does not actually implement the interface itself +(meaning `instanceof` will be `false`). This mode takes advantage of a few quirks in PHP to work and exists for cases +where code cannot be `eval`'d or generated. + +{{% /tab %}} +{{< /tabpane >}} + +### Requests + +Creating an actor proxy is very inexpensive for any mode. There are no requests made when creating an actor proxy object. + +When you call a method on a proxy object, only methods that you implemented are serviced by your actor implementation. +`get_id()` is handled locally, and `get_reminder()`, `delete_reminder()`, etc. are handled by the `daprd`. + +## Actor implementation + +Every actor implementation in PHP must implement `\Dapr\Actors\IActor` and use the `\Dapr\Actors\ActorTrait` trait. This +allows for fast reflection and some shortcuts. Using the `\Dapr\Actors\Actor` abstract base class does this for you, but +if you need to override the default behavior, you can do so by implementing the interface and using the trait. + +## Activation and deactivation + +When an actor activates, a token file is written to a temporary directory (by default this is in +`'/tmp/dapr_' + sha256(concat(Dapr type, id))` in linux and `'%temp%/dapr_' + sha256(concat(Dapr type, id))` on Windows). +This is persisted until the actor deactivates, or the host shuts down. This allows for `on_activation` to be called once +and only once when Dapr activates the actor on the host. + +## Performance + +Actor method invocation is very fast on a production setup with `php-fpm` and `nginx`, or IIS on Windows. Even though +the actor is constructed on every request, actor state keys are only loaded on-demand and not during each request. +However, there is some overhead in loading each key individually. This can be mitigated by storing an array of data in +state, trading some usability for speed. It is not recommended doing this from the start, but as an optimization when +needed. + +## Versioning state + +The names of the variables in the `ActorState` object directly correspond to key names in the store. This means that if +you change the type or name of a variable, you may run into errors. To get around this, you may need to version your state +object. In order to do this, you'll need to override how state is loaded and stored. There are many ways to approach this, +one such solution might be something like this: + +```php +state_version < self::VERSION) { + $value = parent::__get($this->get_versioned_key('key', $this->state_version)); + // update the value after updating the data structure + parent::__set($this->get_versioned_key('key', self::VERSION), $value); + $this->state_version = self::VERSION; + $this->save_state(); + } + } + + // if you upgrade all keys as needed in the method above, you don't need to walk the previous + // keys when loading/saving and you can just get the current version of the key. + + private function get_previous_version(int $version): int { + return $this->has_previous_version($version) ? $version - 1 : $version; + } + + private function has_previous_version(int $version): bool { + return $version >= 0; + } + + private function walk_versions(int $version, callable $callback, callable $predicate): mixed { + $value = $callback($version); + if($predicate($value) || !$this->has_previous_version($version)) { + return $value; + } + return $this->walk_versions($this->get_previous_version($version), $callback, $predicate); + } + + private function get_versioned_key(string $key, int $version) { + return $this->has_previous_version($version) ? $version.$key : $key; + } + + public function __get(string $key): mixed { + return $this->walk_versions( + self::VERSION, + fn($version) => parent::__get($this->get_versioned_key($key, $version)), + fn($value) => isset($value) + ); + } + + public function __isset(string $key): bool { + return $this->walk_versions( + self::VERSION, + fn($version) => parent::__isset($this->get_versioned_key($key, $version)), + fn($isset) => $isset + ); + } + + public function __set(string $key,mixed $value): void { + // optional: you can unset previous versions of the key + parent::__set($this->get_versioned_key($key, self::VERSION), $value); + } + + public function __unset(string $key) : void { + // unset this version and all previous versions + $this->walk_versions( + self::VERSION, + fn($version) => parent::__unset($this->get_versioned_key($key, $version)), + fn() => false + ); + } +} +``` + +There's a lot to be optimized, and it wouldn't be a good idea to use this verbatim in production, but you can get the +gist of how it would work. A lot of it will depend on your use case which is why there's not something like this in +the SDK. For instance, in this example implementation, the previous value is kept for where there may be a bug during an upgrade; +keeping the previous value allows for running the upgrade again, but you may wish to delete the previous value. diff --git a/sdkdocs/php/content/en/php-sdk-docs/php-app/_index.md b/sdkdocs/php/content/en/php-sdk-docs/php-app/_index.md new file mode 100644 index 00000000000..1eab0af4142 --- /dev/null +++ b/sdkdocs/php/content/en/php-sdk-docs/php-app/_index.md @@ -0,0 +1,90 @@ +--- +type: docs +title: "The App" +linkTitle: "App" +weight: 1000 +description: Using the App Class +no_list: true +--- + +In PHP, there is no default router. Thus, the `\Dapr\App` class is provided. It uses +[Nikic's FastRoute](https://github.com/nikic/FastRoute) under the hood. However, you are free to use any router or +framework that you'd like. Just check out the `add_dapr_routes()` method in the `App` class to see how actors and +subscriptions are implemented. + +Every app should start with `App::create()` which takes two parameters, the first is an existing DI container, if you +have one, and the second is a callback to hook into the `ContainerBuilder` and add your own configuration. + +From there, you should define your routes and then call `$app->start()` to execute the route on the current request. + + +```php + $builder->addDefinitions('config.php')); + +// add a controller for GET /test/{id} that returns the id +$app->get('/test/{id}', fn(string $id) => $id); + +$app->start(); +``` + +## Returning from a controller + +You can return anything from a controller, and it will be serialized into a json object. You can also request the +Psr Response object and return that instead, allowing you to customize headers, and have control over the entire response: + +```php + $builder->addDefinitions('config.php')); + +// add a controller for GET /test/{id} that returns the id +$app->get('/test/{id}', + fn( + string $id, + \Psr\Http\Message\ResponseInterface $response, + \Nyholm\Psr7\Factory\Psr17Factory $factory) => $response->withBody($factory->createStream($id))); + +$app->start(); +``` + +## Using the app as a client + +When you just want to use Dapr as a client, such as in existing code, you can call `$app->run()`. In these cases, there's +usually no need for a custom configuration, however, you may want to use a compiled DI container, especially in production: + +```php + $builder->enableCompilation(__DIR__)); +$result = $app->run(fn(\Dapr\DaprClient $client) => $client->get('/invoke/other-app/method/my-method')); +``` + +## Using in other frameworks + +A `DaprClient` object is provided, in fact, all the sugar used by the `App` object is built on the `DaprClient`. + +```php +withSerializationConfig($yourSerializer)->withDeserializationConfig($yourDeserializer); + +// you can also pass it a logger +$clientBuilder = $clientBuilder->withLogger($myLogger); + +// and change the url of the sidecar, for example, using https +$clientBuilder = $clientBuilder->useHttpClient('https://localhost:3800') +``` + +There are several functions you can call before diff --git a/sdkdocs/php/content/en/php-sdk-docs/php-app/php-unit-testing.md b/sdkdocs/php/content/en/php-sdk-docs/php-app/php-unit-testing.md new file mode 100644 index 00000000000..5da0028b41c --- /dev/null +++ b/sdkdocs/php/content/en/php-sdk-docs/php-app/php-unit-testing.md @@ -0,0 +1,285 @@ +--- +type: docs +title: "Unit Testing" +linkTitle: "Unit Testing" +weight: 1000 +description: Unit Testing +no_list: true +--- + +Unit and integration tests are first-class citizens with the PHP SDK. Using the DI container, mocks, stubs, +and the provided `\Dapr\Mocks\TestClient` allows you to have very fine-grained tests. + +## Testing Actors + +With actors, there are two things we're interested in while the actor is under test: + +1. The returned result based on an initial state +2. The resulting state based on the initial state + +{{< tabpane text=true >}} + +{{% tab header="integration test with TestClient" %}} + +Here's an example test a very simple actor that updates its state and returns a specific value: + +```php +state->number % 2 === 0) { + return false; + } + $this->state->number += 1; + + return true; + } +} + +// TheTest.php + +class TheTest extends \PHPUnit\Framework\TestCase +{ + private \DI\Container $container; + + public function setUp(): void + { + parent::setUp(); + // create a default app and extract the DI container from it + $app = \Dapr\App::create( + configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions( + ['dapr.actors' => [TestActor::class]], + [\Dapr\DaprClient::class => \DI\autowire(\Dapr\Mocks\TestClient::class)] + )); + $app->run(fn(\DI\Container $container) => $this->container = $container); + } + + public function testIncrementsWhenOdd() + { + $id = uniqid(); + $runtime = $this->container->get(\Dapr\Actors\ActorRuntime::class); + $client = $this->getClient(); + + // return the current state from http://localhost:1313/reference/api/actors_api/ + $client->register_get("/actors/TestActor/$id/state/number", code: 200, data: 3); + + // ensure it increments from http://localhost:1313/reference/api/actors_api/ + $client->register_post( + "/actors/TestActor/$id/state", + code: 204, + response_data: null, + expected_request: [ + [ + 'operation' => 'upsert', + 'request' => [ + 'key' => 'number', + 'value' => 4, + ], + ], + ] + ); + + $result = $runtime->resolve_actor( + 'TestActor', + $id, + fn($actor) => $runtime->do_method($actor, 'oddIncrement', null) + ); + $this->assertTrue($result); + } + + private function getClient(): \Dapr\Mocks\TestClient + { + return $this->container->get(\Dapr\DaprClient::class); + } +} +``` + +{{% /tab %}} +{{% tab header="unit test" %}} + +```php +state->number % 2 === 0) { + return false; + } + $this->state->number += 1; + + return true; + } +} + +// TheTest.php + +class TheTest extends \PHPUnit\Framework\TestCase +{ + public function testNotIncrementsWhenEven() { + $container = new \DI\Container(); + $state = new TestState($container, $container); + $state->number = 4; + $id = uniqid(); + $actor = new TestActor($id, $state); + $this->assertFalse($actor->oddIncrement()); + $this->assertSame(4, $state->number); + } +} +``` + +{{% /tab %}} + +{{< /tabpane >}} + +## Testing Transactions + +When building on transactions, you'll likely want to test how a failed transaction is handled. In order to do that, you +need to inject failures and ensure the transaction matches what you expect. + +{{< tabpane text=true >}} + +{{% tab header="integration test with TestClient" %}} + +```php +state->begin(); + $this->state->value = "hello world"; + $this->state->commit(); + } +} + +// TheTest.php +class TheTest extends \PHPUnit\Framework\TestCase { + private \DI\Container $container; + + public function setUp(): void + { + parent::setUp(); + $app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) + => $builder->addDefinitions([\Dapr\DaprClient::class => \DI\autowire(\Dapr\Mocks\TestClient::class)])); + $this->container = $app->run(fn(\DI\Container $container) => $container); + } + + private function getClient(): \Dapr\Mocks\TestClient { + return $this->container->get(\Dapr\DaprClient::class); + } + + public function testTransactionFailure() { + $client = $this->getClient(); + + // create a response from {{% ref state_api %}} + $client->register_post('/state/statestore/bulk', code: 200, response_data: [ + [ + 'key' => 'value', + // no previous value + ], + ], expected_request: [ + 'keys' => ['value'], + 'parallelism' => 10 + ]); + $client->register_post('/state/statestore/transaction', + code: 200, + response_data: null, + expected_request: [ + 'operations' => [ + [ + 'operation' => 'upsert', + 'request' => [ + 'key' => 'value', + 'value' => 'hello world' + ] + ] + ] + ] + ); + $state = new MyState($this->container, $this->container); + $service = new SomeService($state); + $service->doWork(); + $this->assertSame('hello world', $state->value); + } +} +``` + +{{% /tab %}} +{{% tab header="unit test" %}} + +```php +state->begin(); + $this->state->value = "hello world"; + $this->state->commit(); + } +} + +// TheTest.php +class TheTest extends \PHPUnit\Framework\TestCase { + public function testTransactionFailure() { + $state = $this->createStub(MyState::class); + $service = new SomeService($state); + $service->doWork(); + $this->assertSame('hello world', $state->value); + } +} +``` + +{{% /tab %}} + +{{< /tabpane >}} diff --git a/sdkdocs/php/content/en/php-sdk-docs/php-pubsub/_index.md b/sdkdocs/php/content/en/php-sdk-docs/php-pubsub/_index.md new file mode 100644 index 00000000000..e4dc661265b --- /dev/null +++ b/sdkdocs/php/content/en/php-sdk-docs/php-pubsub/_index.md @@ -0,0 +1,63 @@ +--- +type: docs +title: "Publish and Subscribe with PHP" +linkTitle: "Publish and Subscribe" +weight: 1000 +description: How to use +no_list: true +--- + +With Dapr, you can publish anything, including cloud events. The SDK contains a simple cloud event implementation, but +you can also just pass an array that conforms to the cloud event spec or use another library. + +```php +post('/publish', function(\Dapr\Client\DaprClient $daprClient) { + $daprClient->publishEvent(pubsubName: 'pubsub', topicName: 'my-topic', data: ['something' => 'happened']); +}); +``` + +For more information about publish/subscribe, check out [the howto]({{% ref howto-publish-subscribe.md %}}). + +## Data content type + +The PHP SDK allows setting the data content type either when constructing a custom cloud event, or when publishing raw +data. + +{{< tabpane text=true >}} + +{{% tab header="CloudEvent" %}} + +```php +data = $xml; +$event->data_content_type = 'application/xml'; +``` + +{{% /tab %}} +{{% tab header="Raw" %}} + +```php +publishEvent(pubsubName: 'pubsub', topicName: 'my-topic', data: $raw_data, contentType: 'application/octet-stream'); +``` + +{{% alert title="Binary data" color="warning" %}} + +Only `application/octet-steam` is supported for binary data. + +{{% /alert %}} + +{{% /tab %}} + +{{< /tabpane >}} + +## Receiving cloud events + +In your subscription handler, you can have the DI Container inject either a `Dapr\PubSub\CloudEvent` or an `array` into +your controller. The former does some validation to ensure you have a proper event. If you need direct access to the +data, or the events do not conform to the spec, use an `array`. diff --git a/sdkdocs/php/content/en/php-sdk-docs/php-serialization.md b/sdkdocs/php/content/en/php-sdk-docs/php-serialization.md new file mode 100644 index 00000000000..99962d40ed8 --- /dev/null +++ b/sdkdocs/php/content/en/php-sdk-docs/php-serialization.md @@ -0,0 +1,55 @@ +--- +type: docs +title: "Custom Serialization" +linkTitle: "Custom Serializers" +weight: 1000 +description: How to configure serialization +no_list: true +--- + +Dapr uses JSON serialization and thus (complex) type information is lost when sending/receiving data. + +## Serialization + +When returning an object from a controller, passing an object to the `DaprClient`, or storing an object in a state store, +only public properties are scanned and serialized. You can customize this behavior by implementing `\Dapr\Serialization\ISerialize`. +For example, if you wanted to create an ID type that serialized to a string, you may implement it like so: + +```php +id; + } +} +``` + +This works for any type that we have full ownership over, however, it doesn't work for classes from libraries or PHP itself. +For that, you need to register a custom serializer with the DI container: + +```php + [SomeClass::class => new SerializeSomeClass()], +]; +``` + +## Deserialization + +Deserialization works exactly the same way, except the interface is `\Dapr\Deserialization\Deserializers\IDeserialize`. diff --git a/sdkdocs/php/content/en/php-sdk-docs/php-state/_index.md b/sdkdocs/php/content/en/php-sdk-docs/php-state/_index.md new file mode 100644 index 00000000000..193bd4dcbb0 --- /dev/null +++ b/sdkdocs/php/content/en/php-sdk-docs/php-state/_index.md @@ -0,0 +1,99 @@ +--- +type: docs +title: "State Management with PHP" +linkTitle: "State management" +weight: 1000 +description: How to use +no_list: true +--- + +Dapr offers a great modular approach to using state in your application. The best way to learn the basics is to visit +[the howto]({{% ref howto-get-save-state.md %}}). + +## Metadata + +Many state components allow you to pass metadata to the component to control specific aspects of the component's +behavior. The PHP SDK allows you to pass that metadata through: + +```php +run( + fn(\Dapr\State\StateManager $stateManager) => + $stateManager->save_state('statestore', new \Dapr\State\StateItem('key', 'value', metadata: ['port' => '112']))); + +// using the DaprClient +$app->run(fn(\Dapr\Client\DaprClient $daprClient) => $daprClient->saveState(storeName: 'statestore', key: 'key', value: 'value', metadata: ['port' => '112'])) +``` + +This is an example of how you might pass the port metadata to [Cassandra]({{% ref setup-cassandra.md %}}). + +Every state operation allows passing metadata. + +## Consistency/concurrency + +In the PHP SDK, there are four classes that represent the four different types of consistency and concurrency in Dapr: + +```php +}} + +{{% tab header="Transaction prefix" %}} + +```php +run(function (TransactionObject $object ) { + $object->begin(prefix: 'my-prefix-'); + $object->key = 'value'; + // commit to key `my-prefix-key` + $object->commit(); +}); +``` + +{{% /tab %}} +{{% tab header="StateManager prefix" %}} + +```php +run(function(\Dapr\State\StateManager $stateManager) { + $stateManager->load_object($obj = new StateObject(), prefix: 'my-prefix-'); + // original value is from `my-prefix-key` + $obj->key = 'value'; + // save to `my-prefix-key` + $stateManager->save_object($obj, prefix: 'my-prefix-'); +}); +``` + +{{% /tab %}} + +{{< /tabpane >}} diff --git a/sdkdocs/php/readme.md b/sdkdocs/php/readme.md new file mode 100644 index 00000000000..e69de29bb2d From 985e4fa98221bf2b55db6e6b7048928e351fa97b Mon Sep 17 00:00:00 2001 From: Marc Duiker Date: Fri, 21 Nov 2025 14:12:47 +0000 Subject: [PATCH 3/3] Ignore link to validate Signed-off-by: Marc Duiker --- sdkdocs/php/content/en/php-sdk-docs/_index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdkdocs/php/content/en/php-sdk-docs/_index.md b/sdkdocs/php/content/en/php-sdk-docs/_index.md index 845f5360518..0d8044c6b7e 100644 --- a/sdkdocs/php/content/en/php-sdk-docs/_index.md +++ b/sdkdocs/php/content/en/php-sdk-docs/_index.md @@ -105,8 +105,10 @@ $app->start(); Initialize dapr with `dapr init` and then start the project with `dapr run -a dev -p 3000 -- php -S 0.0.0.0:3000`. + You can now open a web browser and point it to [http://localhost:3000/hello/world](http://localhost:3000/hello/world) replacing `world` with your name, a pet's name, or whatever you want. + Congratulations, you've created your first Dapr service! I'm excited to see what you'll do with it!