<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>PHP &#8211; Rafael Bernard Araujo</title>
	<atom:link href="https://rafael.bernard-araujo.com/categoria/technology/programming/php-programacao-tecnologia-tecnologia/feed" rel="self" type="application/rss+xml" />
	<link>https://rafael.bernard-araujo.com</link>
	<description>desenvolvendo... while(!success){  try(); }</description>
	<lastBuildDate>Tue, 30 Dec 2025 07:48:35 +0000</lastBuildDate>
	<language>pt-BR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
<site xmlns="com-wordpress:feed-additions:1">21941730</site>	<item>
		<title>Building a Serverless PHP Application with Bref, Symfony, and DynamoDB Session Management</title>
		<link>https://rafael.bernard-araujo.com/building-a-serverless-php-application-with-bref-symfony-and-dynamodb-session-management.php</link>
					<comments>https://rafael.bernard-araujo.com/building-a-serverless-php-application-with-bref-symfony-and-dynamodb-session-management.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Tue, 30 Dec 2025 07:48:35 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Technology]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2283</guid>

					<description><![CDATA[Introduction Serverless apps are fantastic for automatic scaling, but there’s a catch: they expect you to be stateless. Most web applications, however, rely on sessions to remember users and persist state. Traditional PHP session handlers store data on the filesystem, which doesn’t play nicely with ephemeral AWS Lambda instances. Your sessions vanish as soon as [&#8230;]]]></description>
										<content:encoded><![CDATA[<h2>Introduction</h2>
<p>Serverless apps are fantastic for automatic scaling, but there’s a catch: they expect you to be stateless. Most web applications, however, rely on sessions to remember users and persist state. Traditional PHP session handlers store data on the filesystem, which doesn’t play nicely with ephemeral AWS Lambda instances. Your sessions vanish as soon as the instance disappears.</p>
<p>The usual fix? Fire up a Redis cluster. Works, but suddenly you’ve added infrastructure, ongoing maintenance, and extra costs. Your “serverless” app feels a lot less serverless.</p>
<p>What if we could manage sessions <strong>without touching Redis or any other server</strong>?</p>
<p>In this post, we’ll show you how to build a <strong>truly serverless PHP app</strong> using <strong>Bref</strong>, <strong>Symfony</strong>, and <strong>DynamoDB</strong> for session management. Along the way, you’ll see:</p>
<ul>
<li>A <strong>custom DynamoDB-backed session handler</strong> that replaces filesystem sessions</li>
<li>How to deploy your app via <strong>Lambda Function URLs</strong> using AWS CDK</li>
<li>Storing <strong>CSRF tokens in DynamoDB</strong> for fully stateless operation</li>
<li><strong>Single-table design patterns</strong> for efficient multi-entity storage</li>
</ul>
<p>By the end, you’ll know not just <em>how</em> to implement this architecture, but also <em>when</em> it makes sense and what trade-offs you’re accepting.</p>
<h2>The Challenge: Sessions in Serverless</h2>
<p>Before we dive into the solution, let’s understand why traditional PHP sessions fail in serverless environments.</p>
<ol>
<li><strong>Ephemeral Storage</strong>: Lambda instances can vanish at any time. Writing sessions to <code>/tmp</code> is like storing them in sand. They disappear when the instance is recycled.</li>
<li><strong>No Shared Filesystem</strong>: Each Lambda invocation runs on its own instance. User A’s session written by instance 1 is invisible to instance 2. That’s a problem if your user expects to stay logged in.</li>
<li><strong>Horizontal Scaling Woes</strong>: Lambda scales horizontally automatically. Without centralized session storage, each instance is isolated. Consistent session management? Forget it.</li>
</ol>
<h3>The Traditional Solution: Redis/ElastiCache</h3>
<p>Most serverless PHP guides suggest Redis. While it works, it comes with headaches:</p>
<ul>
<li><strong>Infrastructure complexity</strong>: VPCs, subnets, and security groups</li>
<li><strong>Maintenance burden</strong>: Patching, monitoring, capacity planning</li>
<li><strong>Cold start penalty</strong>: VPC-connected Lambdas can take 1–2 extra seconds</li>
</ul>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Better idea</strong>: DynamoDB. It’s fully managed, serverless, and scales automatically. No Redis cluster, no maintenance, just pay for what you use.</p>
<h2>Books and Authors App (Serverless Style)</h2>
<p>Imagine you’re building a multi-tenant SaaS app, like an internal tool for managing books and authors. Each user needs a session, and each organization manages its own data. DynamoDB’s single-table design can elegantly handle all this. Serverless scaling takes care of traffic spikes automatically.</p>
<p>Here’s what this example demonstrates:</p>
<ul>
<li><strong>Multi-entity relationships</strong>: Books belong to authors</li>
<li><strong>CRUD operations</strong>: Create, read, update, and delete across related entities</li>
<li><strong>Session-dependent workflows</strong>: Adding/editing books requires authentication</li>
<li><strong>Real-world complexity</strong>: More than a simple counter, less than a full e-commerce platform</li>
</ul>
<h3>Connecting to Real Use Cases</h3>
<p>This architecture shines in scenarios like:</p>
<ul>
<li><strong>Unpredictable traffic</strong>: Seasonal spikes when authors release new books</li>
<li><strong>Session management</strong>: Authors need persistent sessions to edit content</li>
<li><strong>Cost efficiency</strong>: During quiet periods, you pay pennies; during spikes, DynamoDB scales automatically</li>
<li><strong>Zero maintenance</strong>: No Redis clusters to monitor, no database servers to patch</li>
</ul>
<p>The book management example proves that this approach isn’t just theoretical: it’s production-ready.</p>
<h2>Architecture Overview</h2>
<p>To build a serverless PHP application that supports sessions, CSRF protection, and persistent data, we follow a <strong>stateful/stateless separation</strong> pattern. This makes the architecture scalable, cost-efficient, and easy to maintain.</p>
<h3>1. Stateful Layer: Persistent Data</h3>
<p>This layer is responsible for storing all data that needs to survive beyond a single Lambda invocation.</p>
<ul>
<li>
<p><strong>DynamoDB Table</strong></p>
<ul>
<li>Uses a <strong>single-table design</strong> to store sessions, CSRF tokens, users, books, and authors.</li>
<li><strong>TTL enabled</strong> for automatic session expiration.</li>
<li><strong>On-demand billing</strong> ensures automatic scaling with traffic.</li>
<li>Built-in <strong>multi-AZ replication</strong> provides high availability.</li>
</ul>
</li>
<li>
<p><strong>Benefits</strong></p>
<ul>
<li>No infrastructure to manage or patch.</li>
<li>Automatically scales with unpredictable traffic.</li>
<li>Centralized storage simplifies queries and operations.</li>
</ul>
</li>
</ul>
<h3>2. Stateless Layer: Application Logic</h3>
<p>This layer runs the application code and handles requests without storing any persistent state locally.</p>
<ul>
<li>
<p><strong>Lambda Function</strong></p>
<ul>
<li>Runs <strong>PHP-FPM</strong> via Bref.</li>
<li>Handles HTTP requests directly using a <strong>Lambda Function URL</strong> (HTTPS endpoint).</li>
<li>No VPC required to access DynamoDB, reducing cold start latency.</li>
</ul>
</li>
<li>
<p><strong>Static Assets</strong></p>
<ul>
<li>Stored in <strong>S3</strong> (optionally served via CloudFront) to keep Lambda stateless.</li>
</ul>
</li>
<li>
<p><strong>Benefits</strong></p>
<ul>
<li>Scales automatically with traffic.</li>
<li>Cost-efficient: pay only for actual requests.</li>
<li>Stateless logic simplifies deployment and updates.</li>
</ul>
</li>
</ul>
<p>This design ensures a <strong>truly serverless PHP application</strong> that handles session state, persistent data, and scalable workloads without the operational overhead of managing Redis or other caching layers.</p>
<h2>DynamoDB Session Handler and CSRF Implementation</h2>
<h3>Session Handler Implementation</h3>
<p>In a serverless PHP application, traditional session storage (files or local memory) doesn’t work because Lambda functions are <strong>ephemeral</strong>. Each invocation may run on a different container, so we need a centralized, persistent session store.</p>
<p>The core of our solution is a custom session handler that implements PHP's <code>SessionHandlerInterface</code>.</p>
<h4>How It Works</h4>
<ul>
<li><strong>Sessions are stored in DynamoDB</strong> instead of the filesystem.</li>
<li>Each session has a unique <code>session_id</code>, which becomes the partition key (<code>PK</code>) in DynamoDB.</li>
<li>Sessions include the serialized PHP session data and an <strong>expiration timestamp</strong> (TTL).</li>
<li>The handler automatically reads/writes session data on <code>session_start()</code> and <code>session_write_close()</code>.</li>
</ul>
<h4>Key Features</h4>
<ol>
<li><strong>Automatic Expiration</strong>
<ul>
<li>DynamoDB TTL ensures sessions are removed automatically after expiration.</li>
</ul>
</li>
<li><strong>Atomic Operations</strong>
<ul>
<li><code>PutItem</code> and <code>UpdateItem</code> guarantee consistent writes, even with concurrent requests.</li>
</ul>
</li>
<li><strong>Scalable</strong>
<ul>
<li>Can handle thousands of concurrent sessions without extra infrastructure.</li>
</ul>
</li>
<li><strong>Serverless-friendly</strong>
<ul>
<li>No local storage, no Redis, fully compatible with Lambda statelessness.</li>
</ul>
</li>
</ol>
<h4>Implementation</h4>
<pre><code class="language-php">&lt;?php

namespace App\Session;

use AsyncAws\DynamoDb\DynamoDbClient;
use AsyncAws\DynamoDb\Input\DeleteItemInput;
use AsyncAws\DynamoDb\Input\GetItemInput;
use AsyncAws\DynamoDb\Input\PutItemInput;
use AsyncAws\DynamoDb\ValueObject\AttributeValue;

/**
 * A minimal DynamoDB-backed PHP session handler using AsyncAws.
 *
 * Table design (single-table compatible):
 *  - PK: &quot;SESSION&quot;
 *  - SK: &quot;SID#&lt;session_id&gt;&quot;
 *  - data: base64-encoded session payload (string)
 *  - expiresAt: unix epoch seconds (number), enable DynamoDB TTL on this attribute
 *
 * Garbage collection is handled by DynamoDB&#039;s TTL, so gc() is a no-op.
 */
class DynamoDbSessionHandler implements \SessionHandlerInterface
{
    private const string PK_VALUE = &#039;SESSION&#039;;
    private const string SK_PREFIX = &#039;SID#&#039;;

    public function __construct(
        private readonly DynamoDbClient $dynamoDb,
        private readonly string $tableName,
        private readonly int $ttlSeconds = 3600,
    ) {}

    public function open(string $path, string $name): bool
    {
        // Nothing to do
        return true;
    }

    public function close(): bool
    {
        // Nothing to do
        return true;
    }

    public function read(string $id): string
    {
        $result = $this-&gt;dynamoDb-&gt;getItem(new GetItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Key&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $id]),
            ],
            // Strongly consistent read to reduce stale sessions
            &#039;ConsistentRead&#039; =&gt; true,
        ]));

        $item = $result-&gt;getItem();
        if (!$item || !isset($item[&#039;data&#039;])) {
            return &#039;&#039;;
        }

        $encoded = $item[&#039;data&#039;]-&gt;getS();
        if ($encoded === null) {
            return &#039;&#039;;
        }

        $payload = base64_decode($encoded, true);
        return $payload === false ? &#039;&#039; : $payload;
    }

    public function write(string $id, string $data): bool
    {
        $expiresAt = time() + $this-&gt;ttlSeconds;

        $this-&gt;dynamoDb-&gt;putItem(new PutItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Item&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $id]),
                &#039;data&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; base64_encode($data)]),
                &#039;expiresAt&#039; =&gt; new AttributeValue([&#039;N&#039; =&gt; (string) $expiresAt]),
            ],
        ]));

        return true;
    }

    public function destroy(string $id): bool
    {
        $this-&gt;dynamoDb-&gt;deleteItem(new DeleteItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Key&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $id]),
            ],
        ]));

        return true;
    }

    public function gc(int $max_lifetime): int|false
    {
        // Rely on DynamoDB TTL to expire items; nothing to scan/delete here.
        return 0;
    }
}</code></pre>
<h4>Why it matters?</h4>
<p>This approach:</p>
<ul>
<li>Keeps your PHP sessions serverless-compatible.</li>
<li>Avoids cold-start pitfalls associated with local or in-memory session storage.</li>
<li>Provides a reliable, scalable, and fully managed solution for stateful data in a stateless environment.</li>
</ul>
<h3>CSRF Token Storage</h3>
<p>In a serverless environment, CSRF tokens must be handled carefully. Because Lambda executions are stateless, tokens cannot be stored in memory or on the filesystem. Instead, CSRF tokens are persisted in DynamoDB alongside session data.</p>
<p>This approach ensures tokens remain valid and verifiable across multiple Lambda invocations.</p>
<h4>How CSRF Tokens Are Stored</h4>
<p>Each CSRF token is stored as a dedicated item in the DynamoDB table:</p>
<ul>
<li>Tokens are associated with a specific action</li>
<li>Each token has a unique identifier</li>
<li>An expiration timestamp is stored for automatic cleanup</li>
</ul>
<p>This makes CSRF token storage consistent, durable, and serverless-compatible.</p>
<h4>Data Model</h4>
<p>CSRF tokens follow the same single-table design pattern used elsewhere in the application.</p>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>PK</td>
<td><code>CSRF</code></td>
</tr>
<tr>
<td>SK</td>
<td><code>TOKEN#&lt;token_id&gt;</code></td>
</tr>
<tr>
<td>session</td>
<td><code>&lt;session_id&gt;</code></td>
</tr>
<tr>
<td>expiresAt</td>
<td><code>&lt;timestamp&gt;</code></td>
</tr>
</tbody>
</table>
<p>Using a distinct partition key avoids contention and allows tokens to scale independently from session traffic.</p>
<h4>Lifecycle</h4>
<ol>
<li>A CSRF token is generated when a form is rendered.</li>
<li>The token is persisted in DynamoDB.</li>
<li>On form submission, the token is retrieved and validated.</li>
<li>After validation or expiration, the token is deleted or allowed to expire via TTL.</li>
</ol>
<p>This lifecycle mirrors traditional CSRF handling while remaining compatible with Lambda’s</p>
<h4>Implementation</h4>
<pre><code class="language-php">class DynamoDbCsrfTokenStorage implements CsrfTokenStorageInterface
{
    private const string PK_VALUE = &#039;CSRF&#039;;
    private const string SK_PREFIX = &#039;TOKEN#&#039;;

    public function getToken(string $tokenId): string
    {
        $result = $this-&gt;dynamoDb-&gt;getItem(new GetItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Key&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $tokenId]),
            ],
        ]));

        $item = $result-&gt;getItem();
        return $item[&#039;value&#039;]-&gt;getS() ?? &#039;&#039;;
    }

    public function setToken(string $tokenId, string $token): void
    {
        $expiresAt = time() + $this-&gt;ttlSeconds;

        $this-&gt;dynamoDb-&gt;putItem(new PutItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Item&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $tokenId]),
                &#039;value&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; $token]),
                &#039;expiresAt&#039; =&gt; new AttributeValue([&#039;N&#039; =&gt; (string) $expiresAt]),
            ],
        ]));
    }
}</code></pre>
<p>This ensures CSRF protection works seamlessly across multiple Lambda invocations.</p>
<h2>Symfony Configuration</h2>
<p>Configuring Symfony correctly is key for serverless PHP apps to work reliably with Lambda, DynamoDB, and Bref. Here’s how we set it up.</p>
<h3>1. Session Storage</h3>
<p>We replace the default PHP session handler with our <strong>DynamoDBSessionHandler</strong>:</p>
<pre><code class="language-yaml"># config/packages/framework.yaml
framework:
    session:
        handler_id: App\Session\DynamoDBSessionHandler
        cookie_secure: auto
        cookie_samesite: lax
        cookie_lifetime: 3600  # 1 hour</code></pre>
<p>Notes:</p>
<ul>
<li><code>handler_id</code> points to our custom service.</li>
<li><code>cookie_secure: auto</code> ensures HTTPS enforcement on Lambda URLs or custom domains.</li>
<li><code>cookie_lifetime</code> aligns with DynamoDB TTL for consistency.</li>
</ul>
<h3>2. Service definition</h3>
<p>Register the DynamoDB session handler as a Symfony service:</p>
<pre><code class="language-yaml"># config/services.yaml
services:
  App\Session\DynamoDbSessionHandler:
    arguments:
      $tableName: &#039;%book_table_name%&#039;
      $ttlSeconds: &#039;%env(default:session_ttl_seconds:int:SESSION_TTL)%&#039;</code></pre>
<ul>
<li><code>$tableName</code> comes from environment variables to support multiple environments.</li>
<li><code>$ttl</code> matches the session lifetime for automatic garbage collection.<br />
This configuration tells Symfony to use our custom handler for all session operations. The handler is automatically injected with the DynamoDB client through Symfony's autowiring.</li>
</ul>
<h3>3. RequestContextListener</h3>
<p>To handle dynamic Lambda Function URLs, we register a listener:</p>
<pre><code class="language-yaml"># config/services.yaml
services:
    App\EventListener\RequestContextListener:
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }</code></pre>
<p>Purpose:</p>
<ul>
<li>Ensures Symfony’s URL generator produces correct URLs.</li>
<li>Sets proper scheme and host for redirects, forms, and CSRF validation.</li>
<li>Essential for Lambda Function URLs where host/scheme changes per invocation.</li>
</ul>
<h4>Why It’s Needed</h4>
<p>Lambda Function URLs:</p>
<ul>
<li>Provide a direct HTTPS endpoint (e.g., <code>https://xyz.lambda-url.us-east-1.on.aws/</code>)</li>
<li>Are <strong>dynamic</strong> and unknown at build time</li>
<li>Require Symfony to know the <strong>scheme and host</strong> at runtime to generate correct URLs</li>
</ul>
<p>Without a listener:</p>
<ul>
<li>Redirects may point to HTTP instead of HTTPS</li>
<li>CSRF tokens may fail</li>
<li>Session cookies might be rejected</li>
<li>OAuth or SSO integrations could break</li>
</ul>
<h4>Implementation</h4>
<pre><code class="language-php">&lt;?php

namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RequestContext;

#[AsEventListener(event: KernelEvents::REQUEST, priority: 1024)]
class RequestContextListener
{
    public function __construct(private RequestContext $requestContext) {}

    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event-&gt;isMainRequest()) {
            return;
        }

        $request = $event-&gt;getRequest();

        // Force HTTPS for Lambda URLs and set proper context
        if ($request-&gt;headers-&gt;get(&#039;host&#039;) &amp;&amp; str_contains($request-&gt;headers-&gt;get(&#039;host&#039;), &#039;lambda-url&#039;)) {
            $this-&gt;requestContext-&gt;setScheme(&#039;https&#039;);
            $this-&gt;requestContext-&gt;setHost($request-&gt;headers-&gt;get(&#039;host&#039;));
            $this-&gt;requestContext-&gt;setHttpPort(80);
            $this-&gt;requestContext-&gt;setHttpsPort(443);

            $request-&gt;server-&gt;set(&#039;HTTPS&#039;, &#039;on&#039;);
            $request-&gt;server-&gt;set(&#039;SERVER_PORT&#039;, 443);
            $request-&gt;server-&gt;set(&#039;REQUEST_SCHEME&#039;, &#039;https&#039;);
        }
    }
}</code></pre>
<p><strong>The CDK Output Dilemma:</strong></p>
<pre><code class="language-typescript">// CDK can output the Lambda URL after deployment
new CfnOutput(this, &#039;LambdaURL&#039;, { 
    value: statelessStack.monolithLambdaFunctionUrl.url 
});
// But this value is only known AFTER deployment completes
// You can&#039;t use it as an environment variable in the SAME deployment</code></pre>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tip: This listener is only necessary if you use the Lambda Function URL as the production endpoint. If you use a custom domain, this can be simplified or skipped.</p>
<p>With this configuration, Symfony becomes serverless-ready, maintaining sessions, CSRF protection, and routing behavior seamlessly while leveraging DynamoDB and Lambda.</p>
<h2>Single-Table Design Pattern</h2>
<p>All entities (sessions, CSRF tokens, users, books, authors) live in a <strong>single DynamoDB table</strong>. This simplifies the architecture and enables atomic operations across different entities.</p>
<table>
<thead>
<tr>
<th>Entity</th>
<th>PK</th>
<th>SK</th>
</tr>
</thead>
<tbody>
<tr>
<td>Session</td>
<td><code>SESSION</code></td>
<td><code>SID#&lt;session_id&gt;</code></td>
</tr>
<tr>
<td>CSRF Token</td>
<td><code>CSRF</code></td>
<td><code>TOKEN#&lt;token_id&gt;</code></td>
</tr>
<tr>
<td>Book</td>
<td><code>BOOK-METADATA</code></td>
<td><code>AUTHOR#&lt;author_id&gt;#BOOK#&lt;book_id&gt;</code></td>
</tr>
<tr>
<td>Author</td>
<td><code>AUTHOR-METADATA</code></td>
<td><code>AUTHOR#&lt;author_id&gt;</code></td>
</tr>
<tr>
<td>User</td>
<td><code>USER</code></td>
<td><code>EMAIL#&lt;email&gt;</code></td>
</tr>
</tbody>
</table>
<ul>
<li><strong>Why single-table?</strong>
<ul>
<li>Reduces infrastructure complexity.</li>
<li>Simplifies monitoring and backup.</li>
<li>Supports atomic transactions across multiple entity types.</li>
<li>Aligns with AWS best practices for DynamoDB.</li>
</ul>
</li>
</ul>
<h2>AWS CDK Infrastructure with Bref</h2>
<p>Deploying a serverless Symfony app requires some AWS setup. Using <strong>AWS CDK</strong> with <strong>Bref</strong> makes this smooth, maintainable, and repeatable.</p>
<h3>Why CDK?</h3>
<ul>
<li><strong>Infrastructure as code</strong>: Everything is versioned and reproducible.</li>
<li><strong>Integration with Symfony</strong>: Easy to link environment variables, DynamoDB, and Lambda functions.</li>
<li><strong>Bref-friendly</strong>: Deploy PHP Lambda layers without manually configuring Lambda functions.</li>
</ul>
<h3>Stateful Stack: DynamoDB Table</h3>
<pre><code class="language-ts">import { NestedStack } from &quot;aws-cdk-lib&quot;;
import * as ddb from &quot;aws-cdk-lib/aws-dynamodb&quot;;

export class BlogAppStatefulStack extends NestedStack {
  public readonly ddb: ddb.Table;

  constructor(scope: Construct, id: string, props: MyNestedStackProps) {
    super(scope, id, props);

    this.ddb = new ddb.Table(this, &#039;ddb&#039;, {
      tableName: `${id}-table`,
      partitionKey: { name: &#039;PK&#039;, type: ddb.AttributeType.STRING },
      sortKey: { name: &#039;SK&#039;, type: ddb.AttributeType.STRING },
      billingMode: ddb.BillingMode.PAY_PER_REQUEST,
      deletionProtection: props.shared.environment === &#039;prod&#039;,
      timeToLiveAttribute: &#039;expiresAt&#039;,
    });
  }
}</code></pre>
<p>Key features:</p>
<ul>
<li><strong>Generic Key Schema</strong>: <code>PK</code> and <code>SK</code> enable single-table design</li>
<li><strong>TTL Enabled</strong>: <code>expiresAt</code> attribute automatically removes expired items</li>
<li><strong>Production Protection</strong>: Deletion protection enabled for production environments</li>
</ul>
<h3>Stateless Stack: Lambda Function with Bref</h3>
<pre><code class="language-ts">import { packagePhpCode, PhpFpmFunction } from &quot;@bref.sh/constructs&quot;;
import * as lambda from &quot;aws-cdk-lib/aws-lambda&quot;;
import { FunctionUrl } from &quot;aws-cdk-lib/aws-lambda&quot;;

export class BlogAppStatelessStack extends NestedStack {
  public readonly monolithLambda: PhpFpmFunction;
  public monolithLambdaFunctionUrl: FunctionUrl;

  private createLambda(props: MyNestedStackProps, staticAssetsBucket: Bucket, ddb: ddb.Table) {
    const lambdaEnvironment = {
      APP_ENV: props.shared.environment,
      APP_SECRET: appSecret,
      ASSET_URL: `https://${staticAssetsBucket.bucketDomainName}/`,
      AWS_LAMBDA_LOG_FORMAT: &#039;text&#039;,
      BOOK_TABLE_NAME: `${props.shared.stackPrefix}-StatefulStack-table`,
    };

    const monolithLambda = new PhpFpmFunction(this, &#039;App&#039;, {
      handler: &#039;public/index.php&#039;,
      phpVersion: &#039;8.4&#039;,
      code: packagePhpCode(&#039;php&#039;, {
        exclude: [&#039;.env.local&#039;, &#039;bin/&#039;],
      }),
      functionName: `${props.shared.stackPrefix}-App`,
      timeout: Duration.seconds(28),
      memorySize: Size.gibibytes(2).toMebibytes(),
      environment: lambdaEnvironment,
    });

    // Create Function URL with no authentication
    const monolithLambdaFunctionUrl = monolithLambda.addFunctionUrl({ 
      authType: lambda.FunctionUrlAuthType.NONE 
    });

    // Grant DynamoDB permissions
    ddb.grantReadWriteData(monolithLambda);

    return { monolithLambda, monolithLambdaFunctionUrl };
  }
}</code></pre>
<h3>Lambda Function URL Configuration</h3>
<p>Lambda Function URLs provide a simple HTTPS endpoint without needing API Gateway:</p>
<pre><code class="language-ts">const monolithLambdaFunctionUrl = monolithLambda.addFunctionUrl({ 
  authType: lambda.FunctionUrlAuthType.NONE 
});</code></pre>
<p><strong>Benefits of Lambda URLs:</strong></p>
<ul>
<li><strong>Simplicity</strong>: Direct HTTPS endpoint without API Gateway complexity</li>
<li><strong>Cost</strong>: No API Gateway charges</li>
<li><strong>Performance</strong>: One less hop in the request path</li>
<li><strong>Built-in HTTPS</strong>: Automatic TLS certificate management</li>
</ul>
<p><strong>Configuration Options:</strong></p>
<ul>
<li><code>authType: NONE</code>: Public access (suitable for web applications)</li>
<li><code>authType: AWS_IAM</code>: Requires AWS signature (for service-to-service communication)</li>
</ul>
<h3>Main Stack: Orchestration</h3>
<pre><code class="language-ts">export class BlogApp extends Stack {
  constructor(scope: Construct, id: string, props: MyStackProps) {
    super(scope, id, props);

    const stackPrefix = props.shared.envStackPrefix;

    const statefulStack = new BlogAppStatefulStack(
      this, `${stackPrefix}-StatefulStack`, props
    );

    const statelessStack = new BlogAppStatelessStack(
      this, `${stackPrefix}-StatelessStack`, props, statefulStack
    );

    // Output important values
    new CfnOutput(this, &#039;Lambda&#039;, { 
      value: statelessStack.monolithLambda.functionName 
    });
    new CfnOutput(this, &#039;LambdaURL&#039;, { 
      value: statelessStack.monolithLambdaFunctionUrl.url 
    });
    new CfnOutput(this, &#039;DynamoDb&#039;, { 
      value: statefulStack.ddb.tableName 
    });
  }
}</code></pre>
<h3>Deployment with CDK</h3>
<p>With the infrastructure defined, deploying the application becomes a repeatable and predictable process. This section focuses on <strong>how the application is built, deployed, and updated</strong> using AWS CDK.</p>
<h4>Local Development Environment</h4>
<p>Local development mirrors the production setup as closely as possible while remaining lightweight.</p>
<ul>
<li>Docker is used to provide a consistent PHP environment.</li>
<li>A Makefile abstracts common commands to reduce cognitive load.</li>
<li>Symfony runs locally with the same session and configuration logic used in Lambda.</li>
</ul>
<p>You can run:</p>
<pre><code class="language-bash"># Pre-requisite - source your aws profile
make up</code></pre>
<p>You can check logs via <code>make logs</code>. And get into the container with <code>make bash</code>. The application will be available at <code>http://localhost:8000</code>, but it might fail to load as there is no existent DynamoDB to connect with. You can check local <code>.env</code> file for environment variables.</p>
<h4>Deploying</h4>
<p>Deploy the application using standard CDK commands (inside the container):</p>
<pre><code class="language-bash"># Pre-requisite - Bootstrap CDK if this is your first deployment - npx cdk bootstrap aws://&lt;ACCOUNT_ID&gt;/&lt;REGION&gt;
# Install dependencies
npm run deploy</code></pre>
<p>Alternatively, you can use the <code>Makefile</code> command outsite the container:</p>
<pre><code class="language-bash">make deploy</code></pre>
<h4>What Gets Created</h4>
<p>The deployment creates:</p>
<ol>
<li>DynamoDB table with TTL enabled</li>
<li>Lambda function with PHP 8.4 runtime (via Bref)</li>
<li>Lambda Function URL for HTTPS access</li>
<li>S3 bucket for static assets</li>
<li>IAM roles and permissions</li>
</ol>
<p>The output should be similar to:</p>
<pre><code class="language-bash">BlogApp (sandbox-blog-app): deploying... [1/1]
sandbox-blog-app: creating CloudFormation changeset...

 &#x2705;  BlogApp (sandbox-blog-app)

&#x2728;  Deployment time: 148.76s

Outputs:
BlogApp.AssetsBucket = sandbox-blog-app-sandboxbloga-assetsbucket5cb76180-5lu45xsqvuym
BlogApp.DynamoDb = sandbox-BlogApp-StatefulStack-table
BlogApp.Lambda = sandbox-BlogApp-App
BlogApp.LambdaURL = https://kiv7utcwku6gihqgs4bfkeuzma0oaylo.lambda-url.us-east-1.on.aws/
Stack ARN:
arn:aws:cloudformation:us-east-1:973974862728:stack/sandbox-blog-app/9ba92580-e50b-11f0-a602-0afffb8dc1a9

&#x2728;  Total time: 163.03s</code></pre>
<p>In this case, <code>https://kiv7utcwku6gihqgs4bfkeuzma0oaylo.lambda-url.us-east-1.on.aws/</code> is the Lambda public URL.</p>
<p>When you access the URL, you will see a log-in form. You can use the &quot;Register&quot; link to create a login. Use it and you will be able to manage Authors and Books. Try to log out and access the pages directly.</p>
<p><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/rafael.bernard-araujo.com/wp-content/uploads/2025/12/301225-1.png?w=580&#038;ssl=1" alt="Login" /></p>
<p><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/rafael.bernard-araujo.com/wp-content/uploads/2025/12/301225-2.png?w=580&#038;ssl=1" alt="Register" /></p>
<p><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/rafael.bernard-araujo.com/wp-content/uploads/2025/12/301225-3.png?w=580&#038;ssl=1" alt="Main" /></p>
<p>Internally it will execute a series of commands:</p>
<pre><code class="language-bash"># clean
npm run clean &amp;&amp; \
# execute php packaging including composer install and npm build for symfony
npm run package:sandbox &amp;&amp; \ 
# deploy as a sandbox not requiring approval
NODE_ENV=sandbox cdk deploy --require-approval never</code></pre>
<p>There is a prod version executing <code>make deploy:prod</code>.</p>
<h3>Testing the Session Implementation</h3>
<p>The application includes a test endpoint to verify session persistence:</p>
<pre><code class="language-php">#[Route(&#039;/session-test&#039;, name: &#039;session_test&#039;)]
public function test(Request $request): JsonResponse
{
    $session = $request-&gt;getSession();
    $counter = $session-&gt;get(&#039;counter&#039;, 0);
    $session-&gt;set(&#039;counter&#039;, $counter + 1);

    return new JsonResponse([
        &#039;message&#039; =&gt; &#039;Session test&#039;,
        &#039;session_id&#039; =&gt; $session-&gt;getId(),
        &#039;counter&#039; =&gt; $session-&gt;get(&#039;counter&#039;),
        &#039;handler&#039; =&gt; get_class($session-&gt;getMetadataBag()-&gt;getMetadata(&#039;handler&#039;)),
    ]);
}</code></pre>
<p>Test with curl:</p>
<pre><code class="language-bash"># First request creates session
curl -i -c cookie.txt https://your-lambda-url/session-test

# Subsequent requests increment counter
curl -i -b cookie.txt https://your-lambda-url/session-test
curl -i -b cookie.txt https://your-lambda-url/session-test

# outputs
➜ curl -i -c cookie.txt https://your-lambda-url/session-test
{&quot;message&quot;:&quot;Session incremented&quot;,&quot;session_id&quot;:&quot;02b0c08e1ccd5f3ea015a06c69e29d11&quot;,&quot;counter&quot;:1,&quot;handler&quot;:&quot;App\\Session\\DynamoDbSessionHandler&quot;}%

➜ curl -i -b cookie.txt https://your-lambda-url/session-test
{&quot;message&quot;:&quot;Session incremented&quot;,&quot;session_id&quot;:&quot;02b0c08e1ccd5f3ea015a06c69e29d11&quot;,&quot;counter&quot;:2,&quot;handler&quot;:&quot;App\\Session\\DynamoDbSessionHandler&quot;}%

➜ curl -i -b cookie.txt https://your-lambda-url/session-test
{&quot;message&quot;:&quot;Session incremented&quot;,&quot;session_id&quot;:&quot;02b0c08e1ccd5f3ea015a06c69e29d11&quot;,&quot;counter&quot;:3,&quot;handler&quot;:&quot;App\\Session\\DynamoDbSessionHandler&quot;}%</code></pre>
<h2>Performance Considerations</h2>
<h3>Cold Start Optimization</h3>
<ol>
<li><strong>Memory Allocation</strong>: Using 2GB memory reduces cold start times</li>
<li><strong>Composer Optimization</strong>: <code>--no-dev --optimize-autoloader</code> reduces code size</li>
<li><strong>PHP 8.4</strong>: Latest PHP version with JIT compiler support</li>
</ol>
<h3>DynamoDB Performance</h3>
<ol>
<li><strong>Consistent Reads</strong>: Ensures session consistency at the cost of slightly higher latency</li>
<li><strong>On-Demand Billing</strong>: No capacity planning, automatic scaling</li>
<li><strong>TTL</strong>: Automatic cleanup without scan operations</li>
</ol>
<p>The serverless model's primary advantage is alignment of costs with actual usage, particularly beneficial for applications with variable or unpredictable traffic patterns. However, actual costs vary significantly based on traffic patterns, request complexity, and specific use cases. It's recommended to use AWS cost estimation tools and monitor actual usage to understand the financial impact for your specific application.</p>
<h2>Security Best Practices</h2>
<h3>Session Security</h3>
<ol>
<li><strong>Secure Flag</strong>: Ensures cookies only sent over HTTPS</li>
<li><strong>SameSite</strong>: Protects against CSRF attacks</li>
<li><strong>Regenerate ID</strong>: After authentication to prevent session fixation</li>
</ol>
<pre><code class="language-yaml">framework:
    session:
        cookie_httponly: true
        cookie_secure: auto
        cookie_samesite: lax</code></pre>
<h3>DynamoDB Permissions</h3>
<p>The Lambda function requires minimal permissions:</p>
<pre><code class="language-typescript">ddb.grantReadWriteData(monolithLambda);</code></pre>
<p>This grants only:</p>
<ul>
<li><code>dynamodb:GetItem</code></li>
<li><code>dynamodb:PutItem</code></li>
<li><code>dynamodb:DeleteItem</code></li>
<li><code>dynamodb:Query</code></li>
<li><code>dynamodb:Scan</code></li>
</ul>
<p>No administrative permissions are granted to the Lambda function.</p>
<h2>Limitations</h2>
<p>While serverless PHP with DynamoDB sessions offers compelling advantages, it's important to understand the limitations and trade-offs. Here's an honest assessment of where this architecture may not be the best fit:</p>
<h3>1. Cold Start Latency</h3>
<p><strong>The Reality</strong>: Lambda cold starts can add <strong>1-3 seconds</strong> to the first request after a function has been idle. In practice this occurs for less than 1% of the calls.</p>
<p><strong>Mitigation Strategies</strong>:</p>
<ul>
<li><strong>Provisioned Concurrency</strong>: Pre-warm Lambda instances to eliminate cold starts (adds ~$15/month per instance)</li>
<li><strong>Keep-Warm Pings</strong>: Use CloudWatch Events to invoke functions every 5-10 minutes (adds minimal cost but doesn't help with scaling)</li>
<li><strong>Larger Memory Allocation</strong>: We use 2GB memory which provides faster CPUs, reducing cold start duration</li>
<li><strong>Optimize Code</strong>: Minimize dependencies, use PHP preloading, optimize autoloader</li>
</ul>
<p><strong>When it's acceptable</strong>: Background jobs, internal tools, APIs with relaxed SLAs<br />
<strong>When it's problematic</strong>: User-facing e-commerce, real-time chat, gaming applications</p>
<h3>2. Request Timeout Constraints</h3>
<p><strong>The Reality</strong>: Our configuration uses <strong>28 seconds timeout</strong> (API Gateway compatible), though Lambda supports up to <strong>15 minutes</strong>, which Lambda URLs supports.</p>
<p><strong>Not Suitable For</strong>:</p>
<ul>
<li><strong>Long-running batch jobs</strong>: Data exports, report generation, video processing</li>
<li><strong>Large file uploads</strong>: Direct file uploads over 10MB become unreliable</li>
<li><strong>Complex data migrations</strong>: Multi-step transformations requiring minutes to complete</li>
<li><strong>WebSocket connections</strong>: Not supported by Lambda Function URLs (use API Gateway WebSocket instead)</li>
</ul>
<p><strong>Recommended Alternatives</strong>:</p>
<ul>
<li><strong>Keep Lambda URL</strong>: If API Gateway specific features are not needed, we can use custom domain with Lambda URLs and process up to 15 minutes</li>
<li><strong>AWS Step Functions</strong>: Orchestrate long-running workflows across multiple Lambda invocations</li>
<li><strong>ECS/Fargate</strong>: For truly long-running processes (hours), use containers instead</li>
<li><strong>Presigned S3 URLs</strong>: For large file uploads, let clients upload directly to S3</li>
<li><strong>SQS + Background Workers</strong>: Offload heavy processing to asynchronous queues</li>
</ul>
<h3>3. Session Consistency Edge Cases</h3>
<p><strong>The Reality</strong>: DynamoDB is eventually consistent by default, but we use <code>ConsistentRead: true</code> to mitigate this.</p>
<p><strong>Why We Use ConsistentRead</strong>:</p>
<pre><code class="language-php">&#039;ConsistentRead&#039; =&gt; true,  // Ensures we always get the latest session data</code></pre>
<p><strong>Rare Race Conditions</strong>:<br />
Even with consistent reads, race conditions can occur when:</p>
<ul>
<li><strong>Simultaneous Writes</strong>: User opens multiple tabs, both modify session simultaneously—last write wins</li>
<li><strong>Write-then-Read Timing</strong>: Session written in one Lambda, immediately read by another—minimal delay possible</li>
<li><strong>Cross-Region Scenarios</strong>: If using Global Tables, replication lag can cause stale reads in remote regions</li>
</ul>
<p><strong>Practical Impact</strong>: In 99.9% of cases, consistent reads solve the problem. Edge cases typically affect power users opening many tabs or distributed teams across continents.</p>
<p><strong>Mitigation</strong>: For critical operations (e.g., payment processing), use DynamoDB conditional expressions to ensure atomic updates and detect conflicts.</p>
<h3>4. DynamoDB Costs at Scale</h3>
<p>DynamoDB's pay-per-request pricing is cost-effective at low-to-moderate traffic but pricing characteristics change at high scale.</p>
<h4>Assumptions</h4>
<p><strong>DynamoDB</strong>:</p>
<ul>
<li>On-demand billing: $1.25 per million reads/writes</li>
<li>1KB session item size</li>
<li>1 read + 1 write per request</li>
</ul>
<p><strong>Redis (ElastiCache)</strong>:</p>
<ul>
<li>t4g.medium: $0.037/hr (~$27/month)</li>
<li>1 node sufficient for low-medium traffic</li>
<li>High traffic may require bigger node(s)</li>
</ul>
<h4>Cost Table</h4>
<table>
<thead>
<tr>
<th>Traffic</th>
<th>Requests / Month</th>
<th>DynamoDB Cost</th>
<th>Redis Cost</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Low</td>
<td>1M</td>
<td>$2.50</td>
<td>$27</td>
<td>DynamoDB far cheaper at low traffic</td>
</tr>
<tr>
<td>Medium</td>
<td>10M</td>
<td>$25</td>
<td>$27</td>
<td>Costs roughly similar; DynamoDB slightly lower ops</td>
</tr>
<tr>
<td>High</td>
<td>50M</td>
<td>$125</td>
<td>$108 (cache.m5.large 3 nodes)</td>
<td>Redis may become cheaper with large, sustained traffic, but ops complexity rises</td>
</tr>
</tbody>
</table>
<p><strong>When Fixed Infrastructure (like Redis/ElastiCache) May Become More Cost-Effective</strong>:</p>
<ul>
<li>Sustained high traffic volumes where fixed costs are fully utilized</li>
<li>Long-lived sessions with more reads than writes</li>
<li>Advanced caching features needed beyond simple session storage</li>
</ul>
<p><strong>Hidden DynamoDB Cost Factors</strong>:</p>
<ul>
<li>Consistent reads cost more than eventually consistent reads</li>
<li>Session writes on every request (even if session data unchanged)</li>
<li>AWS free tier limitations after 12 months</li>
</ul>
<p>Start with DynamoDB for simplicity and operational efficiency. Monitor costs monthly as traffic grows. If costs become a concern at high scale, evaluate whether fixed infrastructure or caching optimizations make sense for your specific use case.</p>
<hr />
<p>These limitations are not dealbreakers but they're <strong>trade-offs</strong>. For the right use cases (bursty traffic, cost-sensitive, minimal ops), the benefits far outweigh the drawbacks.</p>
<h2>Conclusion</h2>
<p>Building serverless PHP applications doesn't require sacrificing familiar frameworks or patterns. By implementing a custom DynamoDB session handler, we achieve:</p>
<ul>
<li><strong>Truly serverless architecture</strong>: No Redis, no EFS, pure AWS managed services</li>
<li><strong>Production-ready session management</strong>: Consistent, scalable, and secure</li>
<li><strong>Cost-effective</strong>: Pay only for actual usage</li>
<li><strong>Developer-friendly</strong>: Standard Symfony application with minimal modifications</li>
<li><strong>Type-safe infrastructure</strong>: AWS CDK with TypeScript</li>
<li><strong>Modern PHP</strong>: PHP 8.4 with all latest features</li>
<li><strong>Local development</strong>: Docker-compose for local testing</li>
</ul>
<p>The combination of Bref for Lambda PHP support, Symfony for application framework, and DynamoDB for stateful storage creates a robust, scalable, and maintainable serverless application architecture.</p>
<h3>When Should You Use This Architecture?</h3>
<p><strong>Choose this approach when:</strong></p>
<ul>
<li>Traffic is unpredictable or bursty (blogs, seasonal apps, internal tools)</li>
<li>Cost optimization matters more than absolute performance</li>
<li>Zero operational overhead is a priority</li>
<li>You need automatic scaling without capacity planning</li>
</ul>
<p>Common use cases are:</p>
<ul>
<li>CMS - Blogs, documentation sites, and knowledge bases with infrequent or sporadic traffic, when sudden spikes are scaled automatically and quite periods costs pennies</li>
<li>Admin Panels and Internal Tools - Dashboard interfaces, internal reporting tools, and back-office applications with sporadic usage patterns. DynamoDB maintains session state without requiring Redis or similar infrastructure.</li>
<li>Multi-Tenant SaaS Applications - B2B platforms where each tenant has independent traffic patterns. DynamoDB's single-table design efficiently manages sessions across all tenants without cross-tenant interference.</li>
<li>API Services with Session Requirements - REST APIs that need stateful operations like OAuth flows, multi-step workflows, or temporary data caching. No Redis clusters to maintain, no session cleanup cron jobs to manage. DynamoDB TTL handles everything automatically.</li>
<li>Seasonal Applications - Event registration systems, holiday campaign sites, tax filing applications, and other time-bound services.</li>
<li>Microservices Requiring Session State - Distributed systems where individual services need temporary state management across invocations.</li>
</ul>
<p><strong>Consider alternatives when:</strong></p>
<ul>
<li>You require consistent sub-100ms response times</li>
<li>Traffic is predictable and sustained at high levels (&gt;10M requests/month)</li>
<li>Long-running processes or WebSocket connections are needed</li>
</ul>
<h3>The Bigger Picture</h3>
<p>This implementation demonstrates that <strong>serverless and stateful aren't mutually exclusive</strong>. While serverless advocates often emphasize &quot;stateless functions,&quot; real-world applications need state management. The key is choosing the right state storage mechanism, and DynamoDB proves that managed, serverless databases can handle session management as effectively as traditional infrastructure, with far less operational burden.</p>
<p>Whether you're building a content management system, an internal admin panel, or a multi-tenant SaaS application, this architecture provides a production-ready foundation. Start simple, monitor costs and performance, and scale confidently knowing your infrastructure will grow with your application without requiring a dedicated ops team.</p>
<h2>Resources</h2>
<ul>
<li><a href="https://bref.sh/">Bref Documentation</a></li>
<li><a href="https://github.com/brefphp/constructs">Bref CDK Constructs</a></li>
<li><a href="https://async-aws.com/clients/dynamodb.html">AsyncAws DynamoDB Client</a></li>
<li><a href="https://symfony.com/doc/current/session.html">Symfony Session Documentation</a></li>
<li><a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html">Lambda Function URLs</a></li>
<li><a href="https://www.alexdebrie.com/posts/dynamodb-single-table/">DynamoDB Single-Table Design</a></li>
</ul>
<h2>Source Code</h2>
<p>The complete source code for this application is available at: <a href="https://github.com/rafaelbernard/serverless-php-with-bref-symfony-and-dynamodb-session-management/">rafaelbernard/serverless-php-with-bref-symfony-and-dynamodb-session-management/</a></p>
<p>For detailed technical implementation notes, test coverage reports, and deployment validation, see <a href="https://github.com/rafaelbernard/serverless-php-with-bref-symfony-and-dynamodb-session-management/blob/master/IMPLEMENTATION_SUMMARY.md"><code>IMPLEMENTATION_SUMMARY.md</code></a> in the repository. This document covers:</p>
<ul>
<li>Complete test suite (112 tests across PHP and CDK)</li>
<li>Infrastructure validation details</li>
<li>Code quality metrics</li>
<li>Deployment procedures and best practices</li>
</ul>
<h3><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Bonus: Guide to Custom Domain Configuration with Route53</h3>
<p>Lambda Function URLs provide a quick way to expose your Lambda function over HTTPS, but the auto-generated URL (e.g., <code>https://abc123xyz.lambda-url.us-east-1.on.aws/</code>) isn't branded or memorable. But you can add a Simple CNAME Mapping: Direct Route53 CNAME to Lambda Function URL (easiest, limited SSL control).</p>
<p>This is the <strong>quickest and easiest</strong> method. Just create a CNAME record pointing to your Lambda Function URL. Best for internal tools, prototypes, and non-production environments.</p>
<h4>Prerequisites</h4>
<p>Before configuring custom domains, ensure you have:</p>
<ol>
<li><strong>Domain registered in Route53</strong> (or another registrar with ability to update nameservers)</li>
<li><strong>Hosted Zone created in Route53</strong> for your domain</li>
</ol>
<h4>Implementation with CDK</h4>
<p>Here's how to add a custom domain CNAME record pointing to your Lambda Function URL using AWS CDK:</p>
<pre><code class="language-typescript">import * as route53 from &#039;aws-cdk-lib/aws-route53&#039;;
import * as route53Targets from &#039;aws-cdk-lib/aws-route53-targets&#039;;
import * as lambda from &#039;aws-cdk-lib/aws-lambda&#039;;
import { Construct } from &#039;constructs&#039;;

// Assuming you have a Lambda function with Function URL enabled
const myFunction = new lambda.Function(this, &#039;MyFunction&#039;, {
  // ... function configuration
  functionUrlOptions: {
    authType: lambda.FunctionUrlAuthType.NONE, // or AWS_IAM
  },
});

// Get the hosted zone for your domain
const hostedZone = route53.HostedZone.fromLookup(this, &#039;HostedZone&#039;, {
  domainName: &#039;yourdomain.com&#039;,
});

// Create CNAME record pointing to Lambda URL
new route53.CnameRecord(this, &#039;LambdaUrlCname&#039;, {
  zone: hostedZone,
  recordName: &#039;api&#039;, // Creates api.yourdomain.com
  domainName: cdk.Fn.parseDomainName(myFunction.functionUrl), // Extracts hostname from URL
  ttl: cdk.Duration.minutes(5),
  comment: &#039;CNAME to Lambda Function URL&#039;,
});

// Output the custom domain
new cdk.CfnOutput(this, &#039;CustomDomainUrl&#039;, {
  value: `https://api.yourdomain.com`,
  description: &#039;Custom domain URL for Lambda function&#039;,
});</code></pre>
<h4>Testing Your CNAME Setup</h4>
<p>After creating the CNAME record, verify it works:</p>
<pre><code class="language-bash"># Check DNS propagation
dig api.yourdomain.com

# Test the endpoint
curl -i https://api.yourdomain.com/

# Verify SSL certificate
openssl s_client -connect api.yourdomain.com:443 -servername api.yourdomain.com | grep subject</code></pre>
<p><strong>Expected Results</strong>:</p>
<ul>
<li>DNS query returns Lambda Function URL hostname as CNAME target</li>
<li>HTTP request succeeds with same response as Lambda URL</li>
<li>SSL certificate shows AWS-managed certificate (not your custom domain)</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/building-a-serverless-php-application-with-bref-symfony-and-dynamodb-session-management.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2283</post-id>	</item>
		<item>
		<title>Lambda extension to cache SSM and Secrets Values for PHP Lambda on CDK</title>
		<link>https://rafael.bernard-araujo.com/lambda-extension-to-cache-ssm-and-secrets-values-for-php-lambda-on-cdk.php</link>
					<comments>https://rafael.bernard-araujo.com/lambda-extension-to-cache-ssm-and-secrets-values-for-php-lambda-on-cdk.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Thu, 27 Jun 2024 08:58:29 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[aws]]></category>
		<category><![CDATA[aws-lambda]]></category>
		<category><![CDATA[lambda]]></category>
		<category><![CDATA[serverless]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2038</guid>

					<description><![CDATA[Introduction Managing secrets securely in AWS Lambda functions is crucial for maintaining the integrity and confidentiality of your applications. AWS provides services like AWS Secrets Manager and AWS Systems Manager Parameter Store to manage secrets. However, frequent retrieval of secrets can introduce latency and additional costs. To optimize this, we can cache secrets using a [&#8230;]]]></description>
										<content:encoded><![CDATA[<h1>Introduction</h1>
<p>Managing secrets securely in AWS Lambda functions is crucial for maintaining the integrity and confidentiality of your applications. AWS provides services like AWS Secrets Manager and AWS Systems Manager Parameter Store to manage secrets. However, frequent retrieval of secrets can introduce latency and additional costs. To optimize this, we can cache secrets using a Lambda Extension.</p>
<p>In this article, we will demonstrate how to use a pre-existing Lambda Extension to cache secrets for a PHP Lambda function using the Bref layer and AWS CDK for deployment.</p>
<p>On a high-level, these are the components involved:</p>
<p><a href="https://i0.wp.com/d2908q01vomqb2.cloudfront.net/1b6453892473a467d07372d45eb05abc2031647a/2022/11/17/secrets1.png?ssl=1" title="Components"><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/d2908q01vomqb2.cloudfront.net/1b6453892473a467d07372d45eb05abc2031647a/2022/11/17/secrets1.png?w=580&#038;ssl=1" alt="Lambda Execution Components" title="Components" /></a></p>
<blockquote>
<p><a href="https://aws.amazon.com/blogs/compute/using-the-aws-parameter-and-secrets-lambda-extension-to-cache-parameters-and-secrets/?ref=serverlessland">Using the AWS Parameter and Secrets Lambda extension to cache parameters and secrets</a></p>
<p>The new AWS Parameters and Secrets Lambda extension provides a managed parameters and secrets cache for Lambda functions. The extension is distributed as a Lambda layer that provides an in-memory cache for parameters and secrets. It allows functions to persist values through the <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html#runtimes-lifecycle">Lambda execution lifecycle</a>, and provides a configurable time-to-live (TTL) setting.</p>
<p>When you request a parameter or secret in your Lambda function code, the extension retrieves the data from the local in-memory cache, if it is available. If the data is not in the cache or it is stale, the extension fetches the requested parameter or secret from the respective service. This helps to reduce external API calls, which can improve application performance and reduce cost.</p>
</blockquote>
<h1>Prerequisites</h1>
<ul>
<li>AWS Account</li>
<li>AWS CLI configured</li>
<li>AWS CDK installed</li>
<li>PHP installed</li>
<li>Composer installed</li>
</ul>
<p>If you have <a href="https://docs.docker.com/engine/install/">Docker</a>, all requirements are being installed by it.</p>
<h1>Repository Overview</h1>
<p>The code for this project is available in the following GitHub repository: <a href="https://github.com/rafaelbernard/serverless-patterns/tree/rafaelbernard-feature-lambda-extension-ssm-secrets-cdk-php">rafaelbernard/serverless-patterns</a>. The relevant files are located in the <code>lambda-extension-ssm-secrets-cdk-php</code> folder.</p>
<h1>Step-by-Step Guide</h1>
<h2>1. Cloning the Repository</h2>
<p>First, clone the repository and navigate to the relevant directory:</p>
<pre><code class="language-bash">git clone --branch rafaelbernard-feature-lambda-extension-ssm-secrets-cdk-php https://github.com/rafaelbernard/serverless-patterns.git
cd serverless-patterns/lambda-extension-ssm-secrets-cdk-php</code></pre>
<h2>2. Project Structure</h2>
<p>The project structure is as follows:</p>
<pre><code>.
├── assets
│   └── lambda
│       └── lambda.php
├── bin
│   └── cdk.ts
├── cdk
│   └── cdk-stack.ts
├── cdk.json
├── docker-compose.yml
├── Dockerfile
├── example-pattern.json
├── Makefile
├── package.json
├── package-lock.json
├── php
│   ├── composer.json
│   ├── composer.lock
│   └── handlers
│       └── lambda.php
├── README.md
├── run-docker.sh
└── tsconfig.json</code></pre>
<h2>3. Setting Up the Lambda Function</h2>
<p>The main logic for fetching and caching secrets is in <code>php/handlers/lambda.php</code>:</p>
<pre><code class="language-php">&lt;?php

use Bref\Context\Context;
use Bref\Event\Http\HttpResponse;
use GuzzleHttp\Client;
use Symfony\Component\HttpFoundation\JsonResponse;

// Responsibilities are simplified into one file for demonstration purposes
// We would have have those methods in a Service class

function getParam(string $parameterPath): string
{
    // Set `withDecryption=true if you also want to retrieve SecureString SSMs
    $url = &quot;http://localhost:2773/systemsmanager/parameters/get?name={$parameterPath}&amp;withDecryption=true&quot;;

    try {
        $client = new Client();

        $response = $client-&gt;get($url, [
            &#039;headers&#039; =&gt; [
                &#039;X-Aws-Parameters-Secrets-Token&#039; =&gt; getenv(&#039;AWS_SESSION_TOKEN&#039;),
            ]
        ]);

        $data = json_decode($response-&gt;getBody());
        return $data-&gt;Parameter-&gt;Value;
    } catch (\Exception $e) {
        error_log(&#039;Error getting parameter =&gt; &#039; . print_r($e, true));
    }
}

function getSecret(string $secretName): stdClass
{
    $url = &quot;http://localhost:2773/secretsmanager/get?secretId={$secretName}&quot;;

    try {
        $client = new Client();

        $response = $client-&gt;get($url, [
            &#039;headers&#039; =&gt; [
                &#039;X-Aws-Parameters-Secrets-Token&#039; =&gt; getenv(&#039;AWS_SESSION_TOKEN&#039;),
            ]
        ]);

        $data = json_decode($response-&gt;getBody());
        return json_decode($data-&gt;SecretString);
    } catch (\Exception $e) {
        error_log(&#039;Error getting secretsmanager =&gt; &#039; . print_r($e, true));
    }
}

return function ($request, Context $context) {
    $secret = getSecret(getenv(&#039;THE_SECRET_NAME&#039;));
    $response = new JsonResponse([
        &#039;status&#039; =&gt; &#039;OK&#039;,
        getenv(&#039;THE_SSM_PARAM_PATH&#039;) =&gt; getParam(getenv(&#039;THE_SSM_PARAM_PATH&#039;)),
        getenv(&#039;THE_SECRET_NAME&#039;) =&gt; [
            &#039;password&#039; =&gt; $secret-&gt;password,
            &#039;username&#039; =&gt; $secret-&gt;username,
        ],
    ]);

    return (new HttpResponse($response-&gt;getContent(), $response-&gt;headers-&gt;all()))-&gt;toApiGatewayFormatV2();
};</code></pre>
<h2>4. Setting Up AWS CDK Stack</h2>
<p>The AWS CDK stack is defined in <code>cdk/cdk-stack.ts</code>:</p>
<pre><code class="language-typescript">import { CfnOutput, CfnParameter, Stack, StackProps } from &#039;aws-cdk-lib&#039;;
import { Construct } from &#039;constructs&#039;;
import { join } from &quot;path&quot;;
import { packagePhpCode, PhpFunction } from &quot;@bref.sh/constructs&quot;;
import { FunctionUrlAuthType, LayerVersion, Runtime } from &quot;aws-cdk-lib/aws-lambda&quot;;
import { StringParameter } from &quot;aws-cdk-lib/aws-ssm&quot;;
import { Policy, PolicyStatement } from &#039;aws-cdk-lib/aws-iam&#039;;
import { Secret } from &#039;aws-cdk-lib/aws-secretsmanager&#039;;

export class CdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const stackPrefix = id;

    // May be set as parameter new CfnParameter(this, &#039;parameterStoreExtensionArn&#039;, { type: &#039;String&#039; });
    const parameterStoreExtensionArn = &#039;arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11&#039;;
    const parameterStoreExtension = new CfnParameter(this, &#039;parameterStoreExtensionArn&#039;, { type: &#039;String&#039;, default: parameterStoreExtensionArn });

    const paramTheSsmParam = new StringParameter(this, `${stackPrefix}-TheSsmParam`, {
      parameterName: `/${stackPrefix.toLowerCase()}/ssm/param`,
      stringValue: &#039;the-value-here&#039;,
    });

    // CDK cannot create SecureString
    // You would create the SecureString out of CDK and use the param name here
    // const paramAnSsmSecureStringParam = StringParameter.fromSecureStringParameterAttributes(this, `${stackPrefix}-AnSsmSecureStringParam`, {
    //   parameterName: `/${stackPrefix.toLowerCase()}/ssm/secure-string/params`,
    // });

    const templatedSecret = new Secret(this, &#039;TemplatedSecret&#039;, {
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ username: &#039;postgres&#039; }),
        generateStringKey: &#039;password&#039;,
        excludeCharacters: &#039;/@&quot;&#039;,
      },
    });

    // The param path that will be used to retrieve value by the lambda
    const lambdaEnvironment = {
      THE_SSM_PARAM_PATH: paramTheSsmParam.parameterName,
      THE_SECRET_NAME: templatedSecret.secretName,
      // If you create the SecureString
      // THE_SECURE_SSMPARAM_PATH: paramAnSsmSecureStringParam.parameterName,
    };

    const functionName = `${id}-lambda`;
    const theLambda = new PhpFunction(this, `${stackPrefix}${functionName}`, {
      handler: &#039;lambda.php&#039;,
      phpVersion: &#039;8.3&#039;,
      runtime: Runtime.PROVIDED_AL2,
      code: packagePhpCode(join(__dirname, `../assets/lambda`)),
      functionName,
      environment: lambdaEnvironment,
    });

    // Add extension layer
    theLambda.addLayers(
      LayerVersion.fromLayerVersionArn(this, &#039;ParameterStoreExtension&#039;, parameterStoreExtension.valueAsString)
    );

    // Set additional permissions for parameter store
    theLambda.role?.attachInlinePolicy(
      new Policy(this, &#039;additionalPermissionsForParameterStore&#039;, {
        statements: [
          new PolicyStatement({
            actions: [&#039;ssm:GetParameter&#039;],
            resources: [
              paramTheSsmParam.parameterArn,
              // If you create the SecureString
              // paramAnSsmSecureStringParam.parameterArn,
            ],
          }),
        ],
      }),
    )

    templatedSecret.grantRead(theLambda);

    const fnUrl = theLambda.addFunctionUrl({ authType: FunctionUrlAuthType.NONE });

    new CfnOutput(this, &#039;LambdaUrl&#039;, { value: fnUrl.url });
  }
}</code></pre>
<h2>5. Deploying with AWS CDK</h2>
<p>Make sure you have already AWS variables set and run below command to install required dependancies:</p>
<pre><code class="language-shell"># Using docker -- check run-docker.sh
make up</code></pre>
<p>or</p>
<pre><code class="language-shell"># Using local
npm ci
cd php &amp;&amp; composer install --no-scripts &amp;&amp; cd -</code></pre>
<p>After that, you will have all dependencies installed. Deploy it executing:</p>
<pre><code class="language-shell"># Using docker
make deploy</code></pre>
<p>or</p>
<pre><code class="language-shell"># Using local
npm run deploy</code></pre>
<h2>6. Testing the Lambda Function</h2>
<p>The CDK output will have the Lambda function URL, which you can use to test and retrieve the values:</p>
<pre><code class="language-shell">Outputs:
LambdaExtensionSsmSecretsCdkPhpStack.LambdaUrl = https://keamdws766oqzr6dbiindaix3a0fdojb.lambda-url.us-east-1.on.aws/</code></pre>
<p>You should see the secret value and parameter value returned by the Lambda function. Subsequent invocations should retrieve the values from the cache, reducing latency and cost.</p>
<pre><code class="language-json">{
  &quot;status&quot;: &quot;OK&quot;,
  &quot;/lambdaextensionssmsecretscdkphpstack/ssm/param&quot;: &quot;the-value-here&quot;,
  &quot;TemplatedSecret3D98B577-4jOWSbUMCHmF&quot;: {
    &quot;password&quot;: &quot;!o9GpBzpa&gt;dYdo.Gx3J2!&lt;zd(s-Fg;ev&quot;,
    &quot;username&quot;: &quot;postgres&quot;
  }
}</code></pre>
<h3>Performance benefits</h3>
<p>A similar <a href="https://aws.amazon.com/blogs/compute/using-the-aws-parameter-and-secrets-lambda-extension-to-cache-parameters-and-secrets/">example application written in Python</a> performed three tests, <strong>reducing API calls ~98%</strong>. I am quoting their findings, as the benefits are the same for this PHP Lambda:</p>
<blockquote>
<p>To evaluate the performance benefits of the Lambda extension cache, three tests were run using the open source tool Artillery to load test the Lambda function. </p>
<pre><code class="language-yaml">config:
 target: "https://lambda.us-east-1.amazonaws.com"
phases:
  -
duration: 60
arrivalRate: 10
rampTo: 40</code></pre>
<pre><code>Test 1: The extension cache is disabled by setting the TTL environment variable to 0. This results in 1650 GetParameter API calls to Parameter Store over 60 seconds.</code></pre>
<p>Test 2: The extension cache is enabled with a TTL of 1 second. This results in 106 GetParameter API calls over 60 seconds.<br />
Test 3: The extension is enabled with a TTL value of 300 seconds. This results in only 18 GetParameter API calls over 60 seconds.</p>
<p>In test 3, the TTL value is longer than the test duration. The 18 GetParameter calls correspond to the number of Lambda execution environments created by Lambda to run requests in parallel. Each execution environment has its own in-memory cache and so each one needs to make the GetParameter API call.</p>
<p>In this test, using the extension has <strong>reduced API calls by ~98%</strong>. Reduced API calls results in reduced function execution time, and therefore reduced cost.</p>
</blockquote>
<h2>7. Clean up</h2>
<p>To delete the stack, run:</p>
<pre><code class="language-shell">make bash
npm run destroy</code></pre>
<h1>Conclusion</h1>
<p>In this article, we demonstrated how to use a pre-existing Lambda Extension to cache secrets for a PHP Lambda function using the Bref layer and AWS CDK for deployment. By caching secrets, we can improve the performance and reduce the cost of our serverless applications. The approach detailed here can be adapted to various use cases, enhancing the efficiency of your AWS Lambda functions.</p>
<p>For more information on the Parameter Store, Secrets Manager, and Lambda extensions, refer to:</p>
<ul>
<li><a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html">Using Parameter Store parameters in AWS Lambda functions</a></li>
<li><a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html">Use AWS Secrets Manager secrets in AWS Lambda functions</a></li>
<li><a href="https://aws.amazon.com/blogs/compute/introducing-aws-lambda-extensions-in-preview/">Introducing AWS Lambda Extensions</a></li>
<li><a href="https://aws.amazon.com/blogs/compute/caching-data-and-configuration-settings-with-aws-lambda-extensions/">Caching data and configuration settings with AWS Lambda extensions</a></li>
<li><a href="https://aws.amazon.com/blogs/compute/using-the-aws-parameter-and-secrets-lambda-extension-to-cache-parameters-and-secrets/?ref=serverlessland">AWS blog on using Lambda Extensions to cache secrets</a></li>
</ul>
<p>For more serverless learning resources, visit <a href="https://serverlessland.com/">Serverless Land</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/lambda-extension-to-cache-ssm-and-secrets-values-for-php-lambda-on-cdk.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2038</post-id>	</item>
		<item>
		<title>A bref AWS PHP story – Part 3</title>
		<link>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-3.php</link>
					<comments>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-3.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Tue, 27 Feb 2024 07:44:50 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[aws]]></category>
		<category><![CDATA[aws-cdk]]></category>
		<category><![CDATA[bref]]></category>
		<category><![CDATA[bref-php-aws-story]]></category>
		<category><![CDATA[cdk]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[serverless]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=1980</guid>

					<description><![CDATA[We are starting Part 3 of the Series &#34;A bref AWS PHP history&#34;. You can check Part 1, where I presented the PHP language as a reliable and good alternative for Serverless applications and Part 2 where we see the usage of CDK features in favour of a faithful CI/CD. Part 3 is to show [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>We are starting Part 3 of the Series <a href="https://rafael.bernard-araujo.com/tag/bref-php-aws-story">&quot;A bref AWS PHP history&quot;</a>. You can check <a href="https://dev.to/rafaelbernard/a-bref-aws-php-history-part-1-2agn">Part 1</a>, where I presented the PHP language as a reliable and good alternative for Serverless applications and <a href="https://dev.to/rafaelbernard/a-bref-aws-php-story-part-2-1dhe">Part 2</a> where we see the usage of CDK features in favour of a faithful CI/CD.</p>
<p>Part 3 is to show the upgrade path to Bref 2 and to achieve more coverage of the AWS resources. We will use DynamoDB, a powerful database for serverless architectures.</p>
<p>Some of those topics seem straightforward to some people, but I would like to avoid guessing that this is known to the audience since I have experienced some PHP developers struggling to put all these together for the first time due to the paradigm change. It should be fun.</p>
<p>Table of contents:</p>
<ol>
<li>What else are we doing?</li>
<li>Describing more AWS services - Adding a DynamoDB table</li>
<li>Bref upgrade</li>
<li>Testing CDK</li>
<li>PHP and AWS Services</li>
<li>Wrap-up</li>
</ol>
<h2>What else are we doing?</h2>
<p>In this section, we'll explore additional functionalities and enhancements to our serverless application. Building upon the foundation laid in Part 2, we'll introduce new features and integrations to further extend the capabilities of our AWS PHP application.</p>
<p>The <a href="https://dev.to/rafaelbernard/a-bref-aws-php-story-part-2-1dhe">Part 2</a> uses the result of the Fibonacci of a provided integer or a random integer from 400 to 1000 (to get a good image and not to overflow <code>integer</code>). This integer is the number of pixels of an image from the bucket and an arbitrary request metadata we are creating. If the image does not exist, the lambda will fetch a random image from the web with that number of pixels, save it and generate the metadata.</p>
<p>The computing complexity is irrelevant because it could be very complex logic or very simple, and the topics we are discussing in this part of the series will use the same design.</p>
<p>The lambda will now search the metadata in a DynamoDB table, saving the metadata when it does not exist. DynamoDB is largely used in Lambda code.</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/tree/part-3">Get the part-3 source-code on GitHub</a> and <a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/compare/tag-part-2...tag-part-3">the diff from part-2</a>.</p>
<h2>Describing more AWS services - Adding a DynamoDB Table</h2>
<p>DynamoDB plays a crucial role in serverless architectures, offering scalable and high-performance NoSQL database capabilities. In this section, we'll delve into the process of integrating DynamoDB into our AWS CDK stack, expanding our application's data storage and retrieval capabilities.</p>
<p><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html">DynamoDB</a> is a fully managed NoSQL database service provided by AWS, offering seamless integration with other AWS services, automatic scaling, and built-in security features. Its scalability, low latency, and flexible data model make it well-suited for serverless architectures and applications with varying throughput requirements.</p>
<pre><code class="language-ts">    const table = new Table(this, TableName, {
      partitionKey: { name: &#039;PK&#039;, type: AttributeType.STRING },
      sortKey: { name: &#039;SK&#039;, type: AttributeType.STRING },
      removalPolicy: RemovalPolicy.DESTROY,
      tableName: TableName,
    });</code></pre>
<p>Following the same principles for creating other AWS resources, we utilize the AWS CDK to define a DynamoDB table within our stack. Let's dive into the key parameters of the Table constructor:</p>
<ul>
<li><code>partitionKey</code>: This parameter defines the primary key attribute for the DynamoDB table, used to distribute items across partitions for scalability. In our example, <code>{ name: &#039;PK&#039;, type: AttributeType.STRING }</code> specifies a partition key named 'PK' with a string type. The naming convention ('PK') is arbitrary and can be tailored to suit your application's needs.</li>
<li><code>sortKey</code>: For tables requiring a composite primary key (partition key and sort key), the sortKey parameter comes into play. Here, <code>{ name: &#039;SK&#039;, type: AttributeType.STRING }</code> defines a sort key named 'SK' with a string type. Like the partition key, the name and type of the sort key can be customized based on your data model.</li>
<li><code>removalPolicy</code>: This parameter determines the behaviour of the DynamoDB table when the CloudFormation stack is deleted. By setting <code>RemovalPolicy.DESTROY</code>, we specify that the table should be deleted (destroyed) along with the stack. Alternatively, you can opt for <code>RemovalPolicy.RETAIN</code> to preserve the table post-stack deletion, which may be useful for retaining data.</li>
</ul>
<p>By decoupling configuration from implementation, we adhere to SOLID principles, ensuring cleaner and more robust code. This approach fosters flexibility, allowing our code to seamlessly adapt to changes, such as modifications to the table name while maintaining its functionality.</p>
<p>The implementation code is aware that the name will come from an environment variable and will work with that (yes, if you think that test will be easy to write, you are right):</p>
<pre><code class="language-ts">    const lambdaEnvironment = {
      TableName,
      TableArn: table.tableArn,
      BucketName: brefBucket.bucketName,
    };</code></pre>
<h2>Bref Upgrade</h2>
<p><a href="https://bref.sh">Bref</a>, the PHP runtime for AWS Lambda, continually evolves to provide developers with the latest features and optimizations. In this section, we'll discuss the upgrade to Bref 2.0 and explore how it enhances the deployment process and performance of our serverless PHP applications.</p>
<p>In this section, we're upgrading our usage of <a href="https://bref.sh">Bref</a>, a PHP runtime for AWS Lambda, to version 2.0. Bref simplifies the deployment of PHP applications to AWS Lambda, enabling us to run PHP code serverlessly.</p>
<p>The upgrade involves modifying our AWS CDK code to utilize the new features and improvements introduced in Bref 2.0. One notable improvement is the automatic selection of the latest layer of the PHP version, which simplifies the deployment process and ensures that our Lambda functions run on the most up-to-date PHP environment available.</p>
<pre><code class="language-ts">  const getLambda = new PhpFunction(this, <code>${stackPrefix}${functionName}</code>, {
    handler: 'get.php',
    phpVersion: '8.3',
    runtime: Runtime.PROVIDED_AL2,
    code: packagePhpCode(join(__dirname, <code>../assets/get</code>), {
      exclude: ['test', 'tests'],
    }),
    functionName,
    environment: lambdaEnvironment,
  });</code></pre>
<ul>
<li><strong>`PhpFunction` Constructor</strong>: We&#039;re using the `PhpFunction` constructor provided by Bref to define our Lambda function. This constructor allows us to specify parameters such as the handler file, PHP version, runtime, code location, function name, and environment variables.</li>
<li>`handler`: Specifies the entry point file for our Lambda function, where the execution starts.</li>
<li>`phpVersion`: Defines the PHP version to be used by the Lambda function. In this case, we&#039;re using PHP version 8.3.</li>
<li>`runtime`: Indicates the Lambda runtime environment. Here, `Runtime.PROVIDED_AL2` signifies the use of the Amazon Linux 2 operating system.</li>
<li>`code`: Specifies the location of the PHP code to be deployed to Lambda.</li>
<li>`functionName`: Sets the name of the Lambda function.</li>
<li>`environment`: Allows us to define environment variables required by the Lambda function, such as database connection strings or configuration settings.</li>
</ul>
<p>By upgrading to Bref 2.0 and configuring our Lambda function accordingly, we ensure compatibility with the latest enhancements and optimizations provided by Bref, thereby improving the performance and reliability of our serverless PHP applications on AWS Lambda.</p>
<h2>Testing CDK</h2>
<p>Ensuring the correctness and reliability of our AWS CDK infrastructure is crucial for maintaining a robust serverless architecture. In this section, we&#039;ll delve into testing our CDK resources, focusing on the DynamoDB table we added in the previous section.</p>
<p>As described earlier, we utilized the AWS CDK to provision a DynamoDB table within our serverless stack. Now, let&#039;s ensure that the table is configured correctly and behaves as expected by writing tests using the CDK&#039;s testing framework.</p>
<p>First, let&#039;s revisit how we added the DynamoDB table:</p>
<pre><code class="language-ts">const table = new Table(this, TableName, {
  partitionKey: { name: 'PK', type: AttributeType.STRING },
  sortKey: { name: 'SK', type: AttributeType.STRING },
  removalPolicy: RemovalPolicy.DESTROY,
  tableName: TableName,
});</code></pre>
<p>In this code snippet, we define a DynamoDB table with specified attributes such as partition key, sort key, removal policy, and table name. Now, to ensure that this table is created with the correct configuration, we&#039;ll write tests using CDK&#039;s testing constructs.</p>
<p>Check the following thest:</p>
<pre><code class="language-ts">test('Should have DynamoDB', () => {
  expectCDK(stack).to(
    haveResource(
      'AWS::DynamoDB::Table',
      {
        "DeletionPolicy": "Delete",
        "Properties": {
          "AttributeDefinitions": [
            {
              "AttributeName": "PK",
              "AttributeType": "S",
            },
            {
              "AttributeName": "SK",
              "AttributeType": "S",
            },
          ],
          "KeySchema": [
            {
              "AttributeName": "PK",
              "KeyType": "HASH",
            },
            {
              "AttributeName": "SK",
              "KeyType": "RANGE",
            },
          ],
          "ProvisionedThroughput": {
            "ReadCapacityUnits": 5,
            "WriteCapacityUnits": 5,
          },
          "TableName": "BrefStory-table",
        },
        "Type": "AWS::DynamoDB::Table",
        "UpdateReplacePolicy": "Delete",
      },
      ResourcePart.CompleteDefinition,
    )
  );
});</code></pre>
<p>This test ensures that the DynamoDB table is created with the correct attribute definitions, key schema, provisioned throughput, table name, and other properties specified during its creation. By writing such tests, we validate that our CDK infrastructure is provisioned accurately and functions as intended.</p>
<h2>PHP and AWS Services</h2>
<p>Leveraging PHP in a serverless environment opens up new possibilities for interacting with AWS services. In this section, we&#039;ll examine how PHP code seamlessly integrates with various AWS services, following best practices for maintaining clean and modular code architecture.</p>
<p>This is the part where we have fewer serverless needs impacting the code, as the PHP code will follow the same logic we might be using to communicate with AWS services on any other platform overall (there are always some specific use cases).</p>
<p>The reuse of the same existing logic is excellent. It leverages the decision to keep using PHP when moving that workload to Serverless, as the bulk of the knowledge and already proven code would remain as-is. We may escape the trap of classifying that PHP code as legacy as if it should be avoided, terminated or halted.</p>
<p>As a side note, a few external layers of our software architecture are touched if a good software architecture was applied before. Therefore, during the implementation of this architectural change, it should be quick to realise how beneficial and time-saving it is to have a well-architectured application with a balanced decision for patterns, principles, and designs to be applied, ultimately giving flexibility to the application and its features.</p>
<p>The handler is simplified now and should accommodate everything to a class in the direction of following SRP, a principle that we are bringing to the code during the code bites:</p>
<h3>Applications, domains, infrastructure, etc</h3>
<p>Our `PicsumPhotoService` is still orchestrating the business logic. The Single Responsibility Principle and Inversion of Control are applied. We are injecting the specialized services in the constructor:</p>
<pre><code class="language-php">// readonly class PicsumPhotoService
    public function __construct(
        private HttpClientInterface $httpClient,
        private ImageStorageService $storageService,
        private ImageRepository $repository,
    )
    {
    }</code></pre>
<p>Each specialized service has all its dependencies injected in the constructor as well. We can see the factory instantiation:</p>
<pre><code class="language-php">    public static function createPicsumPhotoService(): PicsumPhotoService
    {
        return new PicsumPhotoService(
            HttpClient::create(),
            new S3ImageService(
                new S3Client(),
                getenv('BucketName'),
            ),
            new DynamoDbImageRepository(
                new DynamoDbClient(),
                getenv('TableName'),
            ),
        );
    }</code></pre>
<p>The `ImageStorageService` will handle all image operations, connecting to the AWS Service when appropriate and observing business logic details. This is a slim interface:</p>
<pre><code class="language-php">interface ImageStorageService
{
    public function getImageFromBucket(int $imagePixels): ?array;

    public function saveImage(int $imagePixels, mixed $fetchedImage): void;

    public function createAndPutMetadata(int $imagePixels, array $metadata): PutObjectOutput;
}</code></pre>
<p>Instead of `: PutObjectOutput`, usually we would return a domain object, to not couple the interface with implementation details of using S3 Services, but for simplicity, I did not create a domain object here. It would be preferable though.</p>
<p>The `ImageRepository` will handle all metadata operations. It will save into a repository and observe logic details as well. Following the same principles, this is a slim interface:</p>
<pre><code class="language-php">interface ImageRepository
{
    public function findImage(int $imagePixels): ImageMetadataItem;

    public function addImageMetadata(ImageMetadataItem $imageMetadataItem): PutItemOutput;
}</code></pre>
<p>The `ImageMetadataItem` is a representation of one of the domain objects we have in our codebase.</p>
<pre><code class="language-php">readonly class ImageMetadataItem
{
    public function __construct(public int $imagePixels, public array $metadata)
    {
    }

    public function toDynamoDbItem(): array
    {
        return [
            'PK' => new AttributeValue(['S' => 'IMAGE']),
            'SK' => new AttributeValue(['S' => "PIXELS#{$this->imagePixels}"]),
            'pixels' => new AttributeValue(['N' => "{$this->imagePixels}"]),
            'metadata' => new AttributeValue(['S' => json_encode($this->metadata)]),
            ...ConvertToDynamoDb::item($this->metadata),
        ];
    }

    /**
     * @param array<string, AttributeValue> $item
     */
    public static function fromDynamoDb(array $item): static
    {
        return new static(
            (int) $item['pixels']->getN(),
            (array) json_decode($item['metadata']->getS()),
        );
    }
}</code></pre>
<p>If you check the implementation details, it operates transparently with all the services, business logic and AWS Services without any high couple with them. There are two utility functions:</p>
<ul>
<li><code>toDynamoDbItem</code>: to transform the object into a valid DynamoDb Item to be added</li>
<li><code>fromDynamoDb</code>: to perform the opposite operation, transforming a DynamoDb Item into a domain object</li>
</ul>
<p>The scope of the operation is very clear and does not bring the domain into dependency on those services, as the domain object can be used independently. It does not block any other way of dealing with it, giving the usage with other types of services, such as different databases or APIs. This is very important to the maintainability of the application without sacrificing the ease of readiness as it keeps the context of the utilities in the right place.</p>
<p>If you check all PHP code carefully, Bref is such a great abstraction layer that, removing the code from the handler file, any other line of code can be used as a lambda or a web application interchangeably without changing any line of code. This is very powerful, as you can imagine how you can leverage and migrate some of the existing code to lambda by just creating a handler that will trigger your existing code, if the code is well structured.</p>
<h2>Wrap-up</h2>
<p>It would be simple like that. Check more details in the source code, install it and try it yourself. This project is ready to:</p>
<ul>
<li>Extend lambda function using Bref</li>
<li>Upgrade to use Bref 2.0</li>
<li>Create a DynamoDB table</li>
<li>Test the stack Cloudformation code</li>
<li>Separate the PHP logic</li>
<li>Have PHP communicating with AWS Services</li>
</ul>
<p>Links:</p>
<ul>
<li><a href="https://rafael.bernard-araujo.com/tag/bref-php-aws-story">https://rafael.bernard-araujo.com/tag/bref-php-aws-story</a></li>
<li><a href="https://bref.sh">https://bref.sh</a></li>
<li><a href="https://dev.to/rafaelbernard/a-bref-aws-php-history-part-1-2agn">https://dev.to/rafaelbernard/a-bref-aws-php-history-part-1-2agn</a></li>
<li><a href="https://dev.to/rafaelbernard/a-bref-aws-php-story-part-2-1dhe">https://dev.to/rafaelbernard/a-bref-aws-php-story-part-2-1dhe</a></li>
<li><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/tree/part-3">https://github.com/rafaelbernard/bref-initial-php-aws-story/tree/part-3</a></li>
<li><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/compare/tag-part-2...tag-part-3">https://github.com/rafaelbernard/bref-initial-php-aws-story/compare/tag-part-2...tag-part-3</a></li>
<li><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html">https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-3.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1980</post-id>	</item>
		<item>
		<title>A bref AWS PHP story – Part 2</title>
		<link>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-2.php</link>
					<comments>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-2.php#comments</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Tue, 21 Mar 2023 13:21:23 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[aws]]></category>
		<category><![CDATA[aws-cdk]]></category>
		<category><![CDATA[bref]]></category>
		<category><![CDATA[bref-php-aws-story]]></category>
		<category><![CDATA[cdk]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[serverless]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=1767</guid>

					<description><![CDATA[We are starting Part 2 of the Series &#34;A bref AWS PHP history&#34;. You can check Part 1, where I presented the PHP language as a reliable and good alternative for Serverless applications. Part 2 is to show how CDK will describe more AWS resource dependencies; how policies and roles are involved in this process; [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>We are starting Part 2 of the Series <a href="https://rafael.bernard-araujo.com/tag/bref-php-aws-story">&quot;A bref AWS PHP history&quot;</a>. You can check <a href="https://dev.to/rafaelbernard/a-bref-aws-php-history-part-1-2agn">Part 1</a>, where I presented the PHP language as a reliable and good alternative for Serverless applications.</p>
<p>Part 2 is to show how CDK will describe more AWS resource dependencies; how policies and roles are involved in this process; how to test if they are applied as expected; and how PHP services will use those resources.</p>
<p>Some of those topics seem straightforward to some people, but I would like to avoid guessing that this is known to the audience since I have experienced some PHP developers struggling to put all these together for the first time due to the paradigm change. It should be fun.</p>
<p>Table of contents:</p>
<ol>
<li>What else are we doing?</li>
<li>Describing more AWS services - Adding an S3 bucket</li>
<li>Services permissions</li>
<li>Testing CDK</li>
<li>PHP and AWS Services
<ol>
<li>Handlers</li>
<li>Application, Domain, Infrastructure, etc</li>
</ol>
</li>
<li>Wrap-up</li>
<li>P.S.: Stats</li>
</ol>
<h2>What else are we doing?</h2>
<p>The <a href="https://dev.to/rafaelbernard/a-bref-aws-php-history-part-1-2agn">Part 1</a> function was returning a Fibonacci result from an int. Very simple. We will keep it simple for now to focus on putting the PHP code into a lambda and allowing PHP code to interact with AWS Services. </p>
<p>The computing complexity is irrelevant because it could be very complex logic or very simple, and the topics we are discussing in this part of the series will use the same design.</p>
<p>The lambda will now use the result of the Fibonacci of a provided integer or a random integer from 400 to 1000 (to get a good image and not to overflow <code>integer</code>). This integer is the number of pixels of an image from the bucket and an arbitrary request metadata we are creating. If the image does not exist, the lambda will fetch a random image from the web with that number of pixels, save it and generate the metadata.</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/tree/part-2">Get the part-2 source-code on GitHub</a> and <a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/compare/tag-part-1...part-2">the diff from part-1</a>.</p>
<h2>Describing more AWS services - Adding an S3 bucket</h2>
<p>S3 buckets are simple yet compelling services for multipurpose workloads. It will be added to the series as a basic storage mechanism. The lambda function, now called <code>GetFibonacciImage</code> function, will need some permissions to manage the bucket.</p>
<p>Starting from the bucket definition, CDK give fantastic constructs, and it goes like this:</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/cdk/cdk-stack.ts#L19-L22">cdk-stack.ts</a></p>
<pre><code class="language-ts">    const brefBucket = new Bucket(this, `${stackPrefix}Bucket`, {
      autoDeleteObjects: true,
      removalPolicy: RemovalPolicy.DESTROY,
    });</code></pre>
<p>By default, buckets will not be deleted during a CDK destroy because they need to be empty. So you will have a hanging bucket in your account. I don't want to keep those contents if the lambda no longer exists. Then <code>autoDeleteObjects</code> and <code>removalPolicy</code> options are selected to enable the destruction of the buckets and their contents if I execute a stack destroy.</p>
<p>We want to decouple the configuration from the implementation to have a more SOLID code. That way, we avoid hard-coded configuration, making our code cleaner and more robust. Then, the code is ready to work, no matter the bucket name.</p>
<p>The implementation code is aware that the name will come from an environment variable and will work with that (yes, if you think that test will be easy to write, you are right):</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/cdk/cdk-stack.ts#L32-L34">cdk-stack.ts</a></p>
<p>and</p>
<pre><code class="language-ts">      environment: {
        BUCKET_NAME: brefBucket.bucketName,
      }</code></pre>
<h2>Services permissions</h2>
<p>There is a Lambda Function and an S3 Bucket. The described use case determines that the lambda needs read and write permissions to the bucket. And nothing more. It is a good practice to give the minimum necessary permission to a resource:</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/cdk/cdk-stack.ts#L37">cdk-stack.ts</a></p>
<pre><code class="language-ts">    brefBucket.grantReadWrite(getLambda);</code></pre>
<p>The result is a list of actions added to the policy recommended by AWS for operations requiring only read and write.</p>
<pre><code class="language-json">          Action: [
            &quot;s3:GetObject*&quot;,
            &quot;s3:GetBucket*&quot;,
            &quot;s3:List*&quot;,
            &quot;s3:DeleteObject*&quot;,
            &quot;s3:PutObject&quot;,
            &quot;s3:PutObjectLegalHold&quot;,
            &quot;s3:PutObjectRetention&quot;,
            &quot;s3:PutObjectTagging&quot;,
            &quot;s3:PutObjectVersionTagging&quot;,
            &quot;s3:Abort*&quot;,
          ],</code></pre>
<h2>Testing CDK</h2>
<p>Testing is a great feature of CDK, and we can see how tests can verify our changes with <code>npm t</code>:</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/test/cdk-stack.test.ts#L17-L22">That there is a function</a></p>
<pre><code class="language-ts">  const functionName = &#039;GetFibonacciImage&#039;;
  /* ... */
  it(&#039;Should have a lambda function to get fibonacci&#039;, () =&gt; {
    template.hasResourceProperties(&#039;AWS::Lambda::Function&#039;, {
      Layers: [Cdk.CdkStack.brefLayerFunctionArn],
      FunctionName: functionName,
    });
  });</code></pre>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/test/cdk-stack.test.ts#L30-L51">And if only the permissions the lambda needs were granted:</a></p>
<pre><code class="language-ts">  it(&#039;Should have a policy for S3&#039;, () =&gt; {
    template.hasResourceProperties(&#039;AWS::IAM::Policy&#039;, {
      PolicyName: Match.stringLikeRegexp(`^${stackPrefix}${functionName}ServiceRoleDefaultPolicy`),
      PolicyDocument: {
        Statement: [{
          Action: [
            &quot;s3:GetObject*&quot;,
            &quot;s3:GetBucket*&quot;,
            &quot;s3:List*&quot;,
            &quot;s3:DeleteObject*&quot;,
            &quot;s3:PutObject&quot;,
            &quot;s3:PutObjectLegalHold&quot;,
            &quot;s3:PutObjectRetention&quot;,
            &quot;s3:PutObjectTagging&quot;,
            &quot;s3:PutObjectVersionTagging&quot;,
            &quot;s3:Abort*&quot;,
          ],
        }],
      },
    });
  });</code></pre>
<p>You may want to check <a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/test/cdk-stack.test.ts">cdk-stack.test.ts</a> to see more details.</p>
<h2>PHP and AWS Services</h2>
<p>This is the part where we have fewer serverless needs impacting the code, as the PHP code will follow the same logic we might be using to communicate with AWS services on any other platform overall (there are always some specific use cases). </p>
<p>The reuse of the same existing logic is excellent. It leverages the decision to keep using PHP when moving that workload to Serverless, as the bulk of the knowledge and already proven code would remain as-is. We may escape the trap of classifying that PHP code as legacy as if it should be avoided, terminated or hated.</p>
<p>As a side note, a few external layers of our software architecture are touched if a good software architecture was applied before. Therefore, during the implementation of this architectural change, it should be quick to realise how beneficial and time-saving it is to have a well-architectured application with a balanced decision for patterns, principles, and designs to be applied, ultimately giving flexibility to the application and its features.</p>
<p>The handler is simplified now and should accommodate everything to a class in the direction of following SRP, a principle that we are bringing to the code during the code bites:</p>
<h3>Handlers</h3>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/php/handlers/get.php">php/handler/get.php</a></p>
<pre><code class="language-php">return function ($request, $context) {
    return \BrefStory\Application\ServiceFactory::createGetFibonacciImageHandler()
        -&gt;handle($request, $context)
        -&gt;toApiGatewayFormatV2();
};</code></pre>
<p>To handle the request details, the Fibonacci code now lives in a proper event handler (<code>implements Bref\Event\Handler</code>).</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/php/src/Event/Handler/GetFibonacciImageHandler.php">php/src/Event/Handler/GetFibonacciImageHandler.php</a></p>
<pre><code class="language-php">    public function handle($event, Context $context): HttpResponse
    {
        $int = (int) (
            $event[&#039;queryStringParameters&#039;][&#039;int&#039;] ?? random_int(
                self::MIN_PIXELS_FOR_REASONABLE_IMAGE_AND_NOT_BIG_FIBONACCI,
                self::MAX_PIXELS_FOR_REASONABLE_IMAGE_AND_NOT_BIG_FIBONACCI
            )
        );

        $metadata = $this-&gt;photoService-&gt;getJpegImageFor($int);

        $responseBody = [
            &#039;context&#039; =&gt; $context,
            &#039;now&#039; =&gt; $this-&gt;dateTimeImmutable()-&gt;format(&#039;Y-m-d H:i:s&#039;),
            &#039;int&#039; =&gt; $int,
            &#039;fibonacci&#039; =&gt; $this-&gt;fibonacci($int),
            &#039;metadata&#039; =&gt; $metadata,
        ];

        $response = new JsonResponse($responseBody);

        return new HttpResponse($response-&gt;getContent(), $response-&gt;headers-&gt;all());
    }</code></pre>
<p>We would also like to start testing the PHP code. As the Event Handler might be a new layer (although very similar to widely used controllers),  <a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/php/tests/unit/Event/Handler/GetFibonacciImageHandlerTest.php">php/tests/unit/Event/Handler/GetFibonacciImageHandlerTest.php</a> test class was created for that. The part-2 will only focus on this test class to avoid overloading with too many changes, but we would usually have test coverage for all the code in the repository.</p>
<h3>Applications, domains, infrastructure, etc</h3>
<p>Finally, we are inside the layers where we are most used to. To fit our purposes, the Event Handler will depend on and call an Application layer service that will orchestrate all the steps to fetch the image metadata.</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/php/src/Application/PicsumPhotoService.php#L34-L42">php/src/Application/PicsumPhotoService.php#L34-L42</a></p>
<pre><code class="language-php">    public function getJpegImageFor(int $imagePixels): array
    {
        try {
            return $this-&gt;getImageFromBucket($imagePixels);
        } catch (NoSuchKeyException) {
            // do nothing
        }

        return $this-&gt;fetchAndSaveImageToBucket($imagePixels);
    }</code></pre>
<p>The interesting thing to mention about using AWS Services is how simple S3Client is instantiated. There is a factory to create service:</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-2/php/src/Application/ServiceFactory.php#L22-L29">php/src/Application/ServiceFactory.php#L22-L29</a></p>
<pre><code class="language-php">    public static function createPicsumPhotoService(): PicsumPhotoService
    {
        return new PicsumPhotoService(
            HttpClient::create(),
            new S3Client(),
            getenv(&#039;BUCKET_NAME&#039;),
        );
    }</code></pre>
<ul>
<li><code>new S3Client</code> is all we need because the environment will use AWS credentials, provided to lambda at execution time, as an assumed role that will carry the policies we defined in the CDK constructs stack, i.e., with read and write permissions to the bucket</li>
<li><code>getenv(&#039;BUCKET_NAME&#039;)</code>, which is gracefully provided by CDK when creating our bucket with any dynamic name it pleases to</li>
</ul>
<p>I asked ChatGPT about this class:</p>
<blockquote>
<p>The <code>PicsumPhotoService</code> class seems to be following the Single Responsibility Principle (SRP) as it has only one responsibility, which is to provide methods for fetching and saving JPEG images from the Picsum website.</p>
<p>The class has methods to fetch the image from an S3 bucket, and if it's not available, fetches it from the Picsum website, saves it to the S3 bucket, and creates and puts metadata for the image in the S3 bucket.</p>
<p>The class has a clear separation of concerns, where the S3Client and HttpClientInterface are injected through the constructor, and the different functionalities are implemented in separate private methods. Additionally, each method is doing a single task, which makes the code easy to read, test, and maintain.</p>
<p>Therefore, it can be concluded that the <code>PicsumPhotoService</code> class follows SRP.</p>
</blockquote>
<h2>Wrap-up</h2>
<p>It would be simple like that. Check more details in the source code, install it and try it yourself. This project is ready to:</p>
<ul>
<li>Create a lambda function using Bref</li>
<li>Create an S3 Bucket with read and write permissions to the lambda</li>
<li>Test the stack Cloudformation code</li>
<li>Separate the PHP logic</li>
<li>Have PHP communicating with AWS Services</li>
<li>Start PHP testing</li>
</ul>
<h2>P.S.: Stats</h2>
<p>I did not plan to talk widely about stats now, but I think I can share the most two significant measures I had with this simple code so far.</p>
<p>[Update 22/03/23] Using <a href="https://k6.io/">https://k6.io/</a></p>
<p>1 - With a brand new stack and a cold lambda:</p>
<pre><code class="language-shell">scenarios: (100.00%) 1 scenario, 200 max VUs, 2m30s max duration (incl. graceful stop):
           * default: 200 looping VUs for 2m0s (gracefulStop: 30s)

     data_received..................: 49 MB  409 kB/s
     data_sent......................: 7.8 MB 65 kB/s
     http_req_blocked...............: avg=2.36ms   min=671ns    med=2.27µs   max=581.87ms p(90)=4.18µs   p(95)=7µs
     http_req_connecting............: avg=712.63µs min=0s       med=0s       max=193.34ms p(90)=0s       p(95)=0s
     http_req_duration..............: avg=531.51ms min=204.46ms med=485.24ms max=3.81s    p(90)=517.98ms p(95)=534.3ms
       { expected_response:true }...: avg=513.6ms  min=204.46ms med=485.07ms max=3.67s    p(90)=516.62ms p(95)=531.5ms
     http_req_failed................: 0.60%  ✓ 272        ✗ 44761
     http_req_receiving.............: avg=123.76µs min=13.77µs  med=44.04µs  max=16.78ms  p(90)=71.27µs  p(95)=85.71µs
     http_req_sending...............: avg=14.79µs  min=4.27µs   med=12.43µs  max=402.74µs p(90)=23.97µs  p(95)=31.4µs
     http_req_tls_handshaking.......: avg=1.37ms   min=0s       med=0s       max=330.58ms p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=531.37ms min=204.36ms med=485.11ms max=3.81s    p(90)=517.77ms p(95)=534.13ms
     http_reqs......................: 45033  373.683517/s
     iteration_duration.............: avg=533.96ms min=204.55ms med=485.34ms max=4.37s    p(90)=518.07ms p(95)=534.4ms
     iterations.....................: 45033  373.683517/s
     vus............................: 200    min=200      max=200
     vus_max........................: 200    min=200      max=200

running (2m00.5s), 000/200 VUs, 45033 complete and 0 interrupted iterations</code></pre>
<p>2 - After the first initial execution, cold lambda and all available images already saved to the bucket, where we got ~3K more requests being served for the same time</p>
<pre><code class="language-shell">scenarios: (100.00%) 1 scenario, 200 max VUs, 2m30s max duration (incl. graceful stop):
           * default: 200 looping VUs for 2m0s (gracefulStop: 30s)

     data_received..................: 53 MB  442 kB/s
     data_sent......................: 8.4 MB 70 kB/s
     http_req_blocked...............: avg=2.26ms   min=631ns    med=2.24µs   max=612.22ms p(90)=4.04µs   p(95)=6.47µs
     http_req_connecting............: avg=663.23µs min=0s       med=0s       max=215.19ms p(90)=0s       p(95)=0s
     http_req_duration..............: avg=490.8ms  min=199.95ms med=484.02ms max=3.17s    p(90)=514.86ms p(95)=527ms
       { expected_response:true }...: avg=490.53ms min=199.95ms med=484.02ms max=2.4s     p(90)=514.85ms p(95)=526.99ms
     http_req_failed................: 0.01%  ✓ 5         ✗ 48754
     http_req_receiving.............: avg=108.86µs min=12.44µs  med=42.68µs  max=17.62ms  p(90)=69.23µs  p(95)=81.87µs
     http_req_sending...............: avg=14.42µs  min=3.9µs    med=12.14µs  max=786.01µs p(90)=23.03µs  p(95)=30.35µs
     http_req_tls_handshaking.......: avg=1.27ms   min=0s       med=0s       max=332.34ms p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=490.68ms min=199.9ms  med=483.91ms max=3.17s    p(90)=514.75ms p(95)=526.89ms
     http_reqs......................: 48759  404.56812/s
     iteration_duration.............: avg=493.16ms min=200.05ms med=484.11ms max=3.17s    p(90)=514.96ms p(95)=527.1ms
     iterations.....................: 48759  404.56812/s
     vus............................: 200    min=200     max=200
     vus_max........................: 200    min=200     max=200

running (2m00.5s), 000/200 VUs, 48759 complete and 0 interrupted iterations</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-2.php/feed</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1767</post-id>	</item>
		<item>
		<title>A bref AWS PHP story &#8211; Part 1</title>
		<link>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-1.php</link>
					<comments>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-1.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Wed, 25 Jan 2023 23:48:51 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[aws]]></category>
		<category><![CDATA[bref]]></category>
		<category><![CDATA[bref-php-aws-story]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[serverless]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=1709</guid>

					<description><![CDATA[The PHP language is a true and good alternative for Serverless applications. PHP is a fast and flexible programming language, and there are many business treasures inside PHP applications, business logic running well for years inside company codebases worldwide. We don't need to look at PHP as a language that could not run inside a [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>The PHP language is a true and good alternative for Serverless applications. PHP is a fast and flexible programming language, and there are many business treasures inside PHP applications, business logic running well for years inside company codebases worldwide.</p>
<p>We don't need to look at PHP as a language that could not run inside a modernized stack. We can move some of this code without total refactoring to Serverless applications, benefiting from an already proven successful code. And we know we all have flows suitable to run as a lambda function.</p>
<p>And not only legacy code. New features are also perfect candidates to be run in PHP and lambdas due to the team's experience, consistency of the technology stack, speed, etc. PHP has served the world well and will remain operating well. PHP is alive.</p>
<p>Table of contents:</p>
<ol>
<li>The series</li>
<li>Functions</li>
<li>Code
<ol>
<li>Requirements</li>
<li>The lambda</li>
</ol>
</li>
<li>Wrap-up</li>
</ol>
<h2>The Serie</h2>
<p>I am starting a series as a walkthrough for PHP into Serverless, specifically to run as lambdas functions.</p>
<p>We will use <a href="https://bref.sh/">Bref</a>, a composer package, to deploy PHP applications to AWS.</p>
<blockquote>
<p>Bref (which means &quot;brief&quot; in french) comes as an open source Composer package and helps you deploy PHP applications to AWS and run them on AWS Lambda.<br />
<a href="https://bref.sh/docs/">https://bref.sh/docs/</a></p>
<p>Bref relies on the Serverless framework and AWS access keys to deploy applications.<br />
<a href="https://bref.sh/docs/installation.html">https://bref.sh/docs/installation.html</a></p>
</blockquote>
<p>The Serverless framework is excellent, but I am more of a fan of <a href="https://docs.aws.amazon.com/cdk/v2/guide/">AWS CDK</a>. Mainly because it is designed to use an imperative programming framework that speeds up the required infrastructure with excellent constructs on different levels (reasonable defaults), and its output can be run against a test framework (predictability).</p>
<p>There are already some CDK constructs for PHP, but, as far as I see, they are intended to be used by Web Apps lambdas (i.e. using frameworks such as Laravel and Symfony). However, the purpose of this series is to run Event-Driven functions, so I will start using pure CDK constructs.</p>
<h2>Functions</h2>
<p>As a walkthrough, we will digest the series in affordable bites, starting from simple functions that we will improve as the series continues and we use more AWS resources.</p>
<h2>Code</h2>
<p>Let's start our PHP lambda function. First, it will begin as an HTTP-based lambda, expecting a request and returning a response. Then, it will execute a trivial piece of computing code: it will return <a href="https://rafael.bernard-araujo.com/high-performance-fibonacci-numbers-generator-in-php.php">fibonacci</a>.</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/tree/part-1">Get the part 1 source-code in GitHub.</a></p>
<h3>Requirements</h3>
<ul>
<li><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-1/.nvmrc">node 18</a></li>
<li><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-1/.php-version">PHP 8.2</a></li>
</ul>
<p><em>(optional) There is a <a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-1/Dockerfile">Dockerfile</a> and a <a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-1/docker-compose.yml">docker-compose.yml</a> file for your convenience if you prefer to use docker. It will require you to set AWS environment variables for use by the container.</em></p>
<h3>The lambda</h3>
<p>You can check the complete <a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/tree/part-1">source code for part 1</a>, and we will highlight essential parts from the CDK code, as the PHP code has a few different things from what we are used to code.</p>
<h4>The stack creator</h4>
<p>In our case, it will create the serverless Stack and related infrastructure, i.e., IAM, lambda function, and URL. If anything else we need, it would be defined and requested by this class.</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-1/cdk/cdk-stack.ts"><code>bin/cdk-stack.ts</code></a></p>
<pre><code class="language-ts">export class CdkStack extends Stack {

  // Get Bref layer ARN from https://runtimes.bref.sh/
  public static brefLayerFunctionArn = &#039;arn:aws:lambda:us-east-1:209497400698:layer:php-82:16&#039;;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const layer = LayerVersion.fromLayerVersionArn(this, &#039;php-layer&#039;, CdkStack.brefLayerFunctionArn);

    const getLambda = new LambdaFunction(this, &#039;get&#039;, {
      layers: [layer],
      handler: &#039;get.php&#039;,
      runtime: Runtime.PROVIDED_AL2,
      code: Code.fromAsset(join(__dirname, `../assets/get`)),
      functionName: &#039;part1-get&#039;,
    });

    const fnUrl = getLambda.addFunctionUrl({authType: FunctionUrlAuthType.NONE});

    new CfnOutput(this, &#039;TheUrl&#039;, {
      // The .url attributes will return the unique Function URL
      value: fnUrl.url,
    });
  }
}</code></pre>
<p><strong>Highlights</strong></p>
<p>The bref php layer:</p>
<pre><code class="language-ts">public static brefLayerFunctionArn = &#039;arn:aws:lambda:us-east-1:209497400698:layer:php-82:16&#039;;</code></pre>
<p>Where you point your entry point and source code:</p>
<pre><code class="language-ts">      handler: &#039;get.php&#039;,
      runtime: Runtime.PROVIDED_AL2,
      code: Code.fromAsset(join(__dirname, `../assets/get`)), // get.php file inside the zip file located at this path</code></pre>
<p>Using AWS Lambda built-int function URL (we will change to API Gateway later if needed):</p>
<pre><code class="language-ts">    const fnUrl = getLambda.addFunctionUrl({authType: FunctionUrlAuthType.NONE});</code></pre>
<p><strong>Output</strong></p>
<p>You would see CDK outputting the lambda function URL you will use to run your application. Something like:</p>
<pre><code class="language-shell">Outputs:
CdkStack.TheUrl = https://6eoftivwkq4ht65d2h2fwlmsga0vnpfs.lambda-url.us-east-1.on.aws/</code></pre>
<h4>The handler</h4>
<p>The PHP entry point has usually named a handler to the code. Its responsibility would be to forward the request to a controller or service that will perform the business rules and prepare the response to be returned. This is an HTTP-based lambda; the response should be an HTTP-valid response.</p>
<p><em>Obs.: You can note by the words above that any existing code that fits in the lambda computing model can be the controller or service to be called by the handler. Theoretically, you only need to create the handler compatible with the lambda environment, instantiate your controller or service, pass whatever it requires as an argument, and then return the expected response.</em></p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-1/php/handlers/get.php"><code>php/handlers/get.php</code></a></p>
<pre><code class="language-php">&lt;?php
return function ($request) {
    $int = (int) ($request[&#039;queryStringParameters&#039;][&#039;int&#039;] ?? random_int(1, 300));

    $responseBody = [
        &#039;response&#039; =&gt; &#039;OK. Time: &#039; . time(),
        &#039;now&#039; =&gt; date(&#039;Y-m-d H:i:s&#039;),
        &#039;int&#039; =&gt; $int,
        &#039;result&#039; =&gt; fibonacci($int),
    ];

    $response = new \Symfony\Component\HttpFoundation\JsonResponse($responseBody);

    return (new \Bref\Event\Http\HttpResponse($response-&gt;getContent(), $response-&gt;headers-&gt;all()))-&gt;toApiGatewayFormatV2();
};</code></pre>
<p><strong>Highlights</strong></p>
<p>All handlers receive a request object. This is how to access <code>/?int=myValue</code> query string param.</p>
<pre><code class="language-php">    $int = (int) ($request[&#039;queryStringParameters&#039;][&#039;int&#039;] ?? random_int(1, 300));</code></pre>
<p>The call to the function <code>fibonacci()</code> is how we would call any other controller or service.</p>
<pre><code class="language-php">&#039;result&#039; =&gt; fibonacci($int),</code></pre>
<p>Using the Symfony Response to validate and prepare a valid HTTP response:</p>
<pre><code class="language-php">$response = new \Symfony\Component\HttpFoundation\JsonResponse($responseBody);</code></pre>
<p>AWS API Gateway requires a certain Response shape. To be sure to have a valid API Gateway response:</p>
<pre><code class="language-php">    return (new \Bref\Event\Http\HttpResponse($response-&gt;getContent(), $response-&gt;headers-&gt;all()))-&gt;toApiGatewayFormatV2();</code></pre>
<p>And that is it. You can now use your lambda function URL as in the output of the CDK stack above and call it with or without the query string param <code>?/int=</code>.</p>
<pre><code class="language-shell">➜   curl https://6eoftivwkq4ht65d2h2fwlmsga0vnpfs.lambda-url.us-east-1.on.aws/
{&quot;response&quot;:&quot;OK. Time: 1674612343&quot;,&quot;now&quot;:&quot;2023-01-25 02:05:43&quot;,&quot;int&quot;:273,&quot;result&quot;:5.05988662735923e+56}%

➜   curl https://6eoftivwkq4ht65d2h2fwlmsga0vnpfs.lambda-url.us-east-1.on.aws/\?int\=500
{&quot;response&quot;:&quot;OK. Time: 1674612353&quot;,&quot;now&quot;:&quot;2023-01-25 02:05:53&quot;,&quot;int&quot;:500,&quot;result&quot;:1.394232245616977e+104}%

➜   curl https://6eoftivwkq4ht65d2h2fwlmsga0vnpfs.lambda-url.us-east-1.on.aws/\?int\=500
{&quot;response&quot;:&quot;OK. Time: 1674612356&quot;,&quot;now&quot;:&quot;2023-01-25 02:05:56&quot;,&quot;int&quot;:500,&quot;result&quot;:1.394232245616977e+104}%</code></pre>
<h4>The test</h4>
<p>We can predict the resources we create via CDK and check if those resources are as expected. The output of the CDK is a CloudFormation template, which we can put under test. That is solid, as unexpected behaviour or changes will fail in our CI pipeline test step.</p>
<p><a href="https://github.com/rafaelbernard/bref-initial-php-aws-story/blob/part-1/test/cdk.test.ts"><code>test/cdk.test.ts</code></a></p>
<pre><code class="language-ts">test(&#039;Lambda created&#039;, () =&gt; {
  const app = new cdk.App();
    // WHEN
  const Stack = new Cdk.CdkStack(app, &#039;MyTestStack&#039;);
    // THEN
  const template = Template.fromStack(stack);

  template.hasResourceProperties(&#039;AWS::Lambda::Function&#039;, {
    Layers: [Cdk.CdkStack.brefLayerFunctionArn]
  });
});</code></pre>
<p><strong>Highlights</strong></p>
<p>We are checking if there is a lambda function and if that function is using the expected specific bref layer:</p>
<pre><code class="language-ts">  template.hasResourceProperties(&#039;AWS::Lambda::Function&#039;, {
    Layers: [Cdk.CdkStack.brefLayerFunctionArn]
  });</code></pre>
<h2>Wrap-up</h2>
<p>We have created our Stack and our first simple HTTP-based PHP lambda function using CDK (with tests). Next, we will improve our lambda to use more AWS resources and communication with more complex application services.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/a-bref-aws-php-story-part-1.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1709</post-id>	</item>
		<item>
		<title>Introducing value objects in PHP</title>
		<link>https://rafael.bernard-araujo.com/introducing-value-objects-in-php.php</link>
					<comments>https://rafael.bernard-araujo.com/introducing-value-objects-in-php.php#comments</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Sun, 06 Mar 2022 21:46:42 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=1480</guid>

					<description><![CDATA[Domain-Driven Design (DDD) is a software design philosophy with one crucial concept: the structure and language of software code (class names, class methods, class variables) should match the business domain. To attend to this concept, DDD presents Value Objects, which, in practice, represents an object similar to a primitive type but should be modelled after [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><a href="https://en.wikipedia.org/wiki/Domain-driven_design">Domain-Driven Design (DDD)</a> is a software design philosophy with one crucial concept: the structure and language of software code (class names, class methods, class variables) should match the business domain. To attend to this concept, DDD presents <a href="https://martinfowler.com/bliki/ValueObject.html"><strong>Value Objects</strong></a>, which, in practice, <strong>represents an object similar to a primitive type but should be modelled after the domain's business rules</strong>.</p>
<h2>What does it mean?</h2>
<p><em>(Checkout code at <a href="https://github.com/rafaelbernard/blog-value-objects">https://github.com/rafaelbernard/blog-value-objects</a>)</em></p>
<p>Value Objects are first described in Evans' <a href="http://amzn.to/1Lkgs7B">Domain-Driven Design book</a>, and further explained in Smith and Lerman's <a href="https://www.pluralsight.com/courses/fundamentals-domain-driven-design">Domain-Driven Design Fundamentals course</a>. It is an immutable type that is distinguishable only by the state of its properties. Unlike an <a href="https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model#Entity%E2%80%93relationship_model"><em>Entity</em></a><sup><a href="https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model#Entity%E2%80%93relationship_model">1</a></sup>, a class type with a unique identifier and remains distinct even if its properties are otherwise identical, two Value Objects with the same properties can be considered equal.</p>
<p>We would use Value Object classes to represent a type strictly and encapsulate the validation rules of that type. Think of age or a card from a poker deck. They can seem sufficiently represented by primitive types such as integer and string, but in reality, they are managed by strict <a href="https://en.wikipedia.org/wiki/Business_rule">business rules</a><sup><a href="https://en.wikipedia.org/wiki/Business_rule">2</a></sup>. For example, you would like to ensure that a particular age value is a non-negative integer. Or a picked card has a value not greater than 10, not 1, i.e., there is a range of allowed numbers or unique letters combined within four allowed suits.</p>
<p>To translate it to code, you could think of a Person class as a guest application to map a person's name and age. Usually, we would say a name is a string and age is an int.</p>
<pre><code class="language-php">class Person
{
    private string $name;
    private int $age;

    public function __constructor(string $name, int $age)
    {
        $this-&gt;name = $name;
        $this-&gt;age = $age;
    }
}</code></pre>
<p>Although we can think that the specification above is good and we are just instantiating a Person object with all the required attributes, we may have some problems with the implementation because negative values are also <code>int</code> and would be allowed:</p>
<pre><code class="language-php">$personOk = new Person(&#039;John Doe&#039;, 18);
$notARealPerson = new Person(&#039;Benjamin Button&#039;, -18); // -18 year? no way!</code></pre>
<p>The code above will not fail and is very common that some business logic is performed to assert age will never be negative:</p>
<pre><code class="language-php">// if from a form
$age = (int) $_POST[&#039;age&#039;];

if ($age &lt; 0) {
    throw new \Exception(&#039;Age could not be negative&#039;);
}

// but you need to copy the check everywhere a Person class is used. What if someone overlooks it?
$person = new Person(&#039;John Doe&#039;, $age);</code></pre>
<p>When using Value Objects, you leverage your class with the exact type you need, correctly applying business logic. I will give more details later, but a hint of how the Person class would be with a Value Object:</p>
<pre><code class="language-php">use App\Domain;

class Person
{
    private string $name;
    private Age $age;

    public function __construct(string $name, Age $age)
    {
        $this-&gt;name = $name;
        $this-&gt;age = $age;
    }
}</code></pre>
<p>And now we are always sure that Person objects will have a valid age. Bear with me and understand how <code>Age</code> Value Object class should look.</p>
<h2>Samples of Value Objects</h2>
<p>You may have already realized that we have many cases where we need similar things on every application. They are used to validate patterns to an expected format, a set of possible values to simulate an enum (only present on PHP 8) or to extend this set of values to be validated against some more rules.</p>
<p>I will be using PHP 7.4 compatible code in this blog. If you have applications using older versions, changing them should not be too complicated. Let me know in the comments if you need samples about how to write to an older version.</p>
<p>Some examples:</p>
<h3>Validating a format:</h3>
<p>Our <code>Age</code> class should be implemented as:</p>
<pre><code class="language-php">&lt;?php

namespace App\Domain;

class Age
{
    private int $value;

    /**
     * @param int $value
     */
    public function __construct(int $value)
    {
        if (!$value &lt; 0) {
            throw new \UnexpectedValueException(&#039;Negative numbers are not a valid age.&#039;);
        }

        $this-&gt;value = $value;
    }

    public function value(): int
    {
        return $this-&gt;value;
    }
}</code></pre>
<p>Or a richer example with card suits:</p>
<pre><code class="language-php">&lt;?php

namespace App\Domain;

class CardSuit
{
    const HEARTS = &#039;H&#039;;
    const DIAMONDS = &#039;D&#039;;
    const CLUBS = &#039;C&#039;;
    const SPADES = &#039;S&#039;;

    const SUITS = [
        self::HEARTS,
        self::DIAMONDS,
        self::CLUBS,
        self::SPADES,
    ];

    private string $value;

    public function __construct(string $value)
    {
        if (!in_array($value, self::SUITS)) {
            throw new \InvalidArgumentException(&quot;`$value` is not a valid card suit.&quot;);
        }

        $this-&gt;value = $value;
    }

    public function __toString()
    {
        return $this-&gt;value;
    }

    public function value(): string
    {
        return $this-&gt;value;
    }
}
</code></pre>
<h3>Validating a set and its rules:</h3>
<p>Observe rules when checking grade with <code>isAlumni()</code> from a given <code>ClassYear</code>. Ideally, we would have a Grade class, but I kept it more straightforward to simplify understanding the check, and I have a similar example with poker cards classes above.</p>
<pre><code class="language-php">&lt;?php

namespace App\Domain;

class ClassYear
{
    private int $value;

    /**
     * @param int $value
     */
    public function __construct(int $value)
    {
        // For class year, our business rules is from 1901-2999
        if (!preg_match(&#039;/^((19\d{2})|(2)\d{3})$/&#039;, $value)) {
            throw new \InvalidArgumentException(&#039;Invalid year&#039;);
        }

        $this-&gt;value = $value;
    }

    public static function fromNow(): self
    {
        return new self(date(&#039;Y&#039;));
    }

    public function value(): int
    {
        return $this-&gt;value;
    }

    public function isAlumni(): bool
    {
        return date(&#039;Y&#039;) - $this-&gt;value &gt;= 13;
    }
}
</code></pre>
<h3>Enum-like validation:</h3>
<p>Observe that it encapsulates some Enum values, but also a simple rule (<code>isGreaterThan</code>), and it enriches the type with a simple business rule support that the application can also use.</p>
<pre><code class="language-php">&lt;?php

namespace App\Domain;

class CardRank
{
    const ACE = &#039;A&#039;;
    const KING = &#039;K&#039;;
    const QUEEN = &#039;Q&#039;;
    const JACK = &#039;J&#039;;
    const TEN = &#039;X&#039;;
    const NINE = &#039;9&#039;;
    const EIGHT = &#039;8&#039;;
    const SEVEN = &#039;7&#039;;
    const SIX = &#039;6&#039;;
    const FIVE = &#039;5&#039;;
    const FOUR = &#039;4&#039;;
    const THREE = &#039;3&#039;;
    const TWO = &#039;2&#039;;

    const RANKS = [
        self::ACE,
        self::KING,
        self::QUEEN,
        self::JACK,
        self::TEN,
        self::NINE,
        self::EIGHT,
        self::SEVEN,
        self::SIX,
        self::FIVE,
        self::FOUR,
        self::THREE,
        self::TWO
    ];

    const WEIGHTS = [
        self::ACE =&gt; 20,
        self::KING =&gt; 13,
        self::QUEEN =&gt; 12,
        self::JACK =&gt; 11,
        self::TEN =&gt; 10,
        self::NINE =&gt; 9,
        self::EIGHT =&gt; 8,
        self::SEVEN =&gt; 7,
        self::SIX =&gt; 6,
        self::FIVE =&gt; 5,
        self::FOUR =&gt; 4,
        self::THREE =&gt; 3,
        self::TWO =&gt; 2
    ];

    private string $value;

    public function __construct(string $value)
    {
        if (!in_array($value, self::RANKS)) {
            throw new \InvalidArgumentException(&quot;`$value` is not a valid card rank.&quot;);
        }

        $this-&gt;value = $value;
    }

    public function __toString()
    {
        return $this-&gt;value;
    }

    public function value(): string
    {
        return $this-&gt;value;
    }

    public function weight()
    {
        return self::WEIGHTS[$this-&gt;value];
    }

    public function isGreaterThan(CardRank $cardRank): bool
    {
        return $this-&gt;weight() &gt; $cardRank-&gt;weight();
    }

    // Some static helper functions can be created to be more readable

    public static function two(): CardRank
    {
        return new self(self::TWO);
    }

    public static function ace(): CardRank
    {
        return new self(self::ACE);
    }
}
</code></pre>
<h2>When to use it</h2>
<p>Create and use the Value Objects whenever you see that it fits &quot;encapsulate the business rules for a given type&quot; and &quot;it represents an object similar to a primitive type&quot;. This will directly understand the expected type of code and instantly validate capabilities.</p>
<p>Some examples:</p>
<pre><code class="language-php">// a
$person = new Person(&#039;John Doe&#039;, new Age(20));
public function saveGuest(Person $person);

// b
$currentUserClassYear = ClassYear::fromNow();
if (!$currentUserClassYear-&gt;isAlumni()) {
    // do something for a non-alumni user
}

// c
$pickedCard = new Card(new CardSuit(CardSuit::SPADES), new CardRank(CardRank::ACE));

// d
$aceSpade = new Card(CardSuit::spades(), CardRank::ace());
$twoSpade = new Card(CardSuit::spades(), CardRank::two());
if ($aceSpace-&gt;isGreaterThan($twoSpade)) {
    // do something when greater, such as sum the weight to count points
}
</code></pre>
<p>A code-base that uses Value Objects avoids repetition code to validate a type that is not represented by native types, improves readability, and keeps consistency about business rules (no one could overlook or forget to &quot;copy the validation code check&quot;). Value Objects has a built-in validation, making it superior to validation classes and creating a relationship with your business domain rules. It then keeps the code clean and lean.</p>
<p>You can see all related code at <a href="https://github.com/rafaelbernard/blog-value-objects">https://github.com/rafaelbernard/blog-value-objects</a>.</p>
<h3>Glossary</h3>
<ol>
<li><a href="https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model#Entity%E2%80%93relationship_model">Entity</a>: An entity may be defined as a thing capable of an independent existence that can be uniquely identified. An entity is an abstraction from the complexities of a domain. When we speak of an entity, we normally speak of some aspect of the real world that can be distinguished from other aspects of the real world. [(from Wikipedia)]</li>
<li><a href="https://en.wikipedia.org/wiki/Business_rule">Business rule</a>: A business rule defines or constrains some aspect of business and always resolves to either true or false. Business rules are intended to assert business structure or to control or influence the behavior of the business.</li>
<li><a href="https://en.wikipedia.org/wiki/Object-oriented_programming">Object-oriented programming</a> (OOP) is a programming paradigm based on the concept of &quot;objects&quot;, which can contain data and code: data in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods).</li>
</ol>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/introducing-value-objects-in-php.php/feed</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1480</post-id>	</item>
		<item>
		<title>PHP 8.1: more on new in initializers</title>
		<link>https://rafael.bernard-araujo.com/php-8-1-more-on-new-in-initializers.php</link>
					<comments>https://rafael.bernard-araujo.com/php-8-1-more-on-new-in-initializers.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Wed, 05 Jan 2022 22:20:47 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[design principles]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[php-8.1]]></category>
		<category><![CDATA[software engineering]]></category>
		<category><![CDATA[software testing]]></category>
		<category><![CDATA[SOLID]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=1456</guid>

					<description><![CDATA[I could not agree more with Brent when he says concerning the &#34;new in initializers&#34;[1] feature: PHP 8.1 adds a feature that might seem like a small detail, but one that I think will have a significant day-by-day impact on many people. When I see this new feature, lots of places that use Dependency Injection[3] [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>I could not agree more with <a href="https://stitcher.io/blog/php-81-new-in-initializers">Brent</a> when he says concerning the &quot;new in initializers&quot;<sup>[1]</sup> feature:</p>
<blockquote>
<p>PHP 8.1 adds a feature that might seem like a small detail, but one that I think will have a significant day-by-day impact on many people.</p>
</blockquote>
<p>When I see this new feature, lots of places that use Dependency Injection<sup>[3]</sup> come to my focus as candidates to be impacted, such as application or infrastructure service classes. As a result, we will write a much cleaner and leaner code without giving up on good practices to write modular, maintainable and testable software.</p>
<p>The Dependency Inversion Principle<sup>[4]</sup> gives us decoupling powers. But we know that many classes will receive the same concrete implementation most of (if not all) the time.</p>
<p>So this is very common to see some variation of this code:</p>
<pre><code class="language-php">$someDependencyToBeInjected = FactoryClass::create();
$someService = new SomeServiceClass($someDependencyToBeInjected);</code></pre>
<p><em>Important note: I will ignore for now Service Containers and frameworks features that deal with service instantiation, auto wiring, etc.</em></p>
<p>Think of a database query service class: you depend on a connection object. Every time you need to instantiate your database service class, you need to prepare the connection dependency and inject it at the service class. Database connections are a great example when you use the same concrete implementation more than 90% of the time. </p>
<p>The same applies to a Service class that you use to handle business logic and depends on QueryService and CommandHandler interfaces to do its job.</p>
<p>Before PHP 8.1 we have code like this:</p>
<pre><code class="language-php">// service class to apply business logic
// most standard
class DefaultLeadRecordService implements LeadRecordService
{
    public function __construct(
        private LeadQueryService $queryService,
        private LeadCommandHandler $commandHandler
    ) {
    }
}

// infrastructure class to match the interface -- a DBAL concrete class
// sneakily allowing a &quot;default&quot; value, but also open to Dependency Injection
// but not that great
class DbalLeadQueryService implements LeadQueryService
{
    public function __construct(private ?Connection $connection = null)
    {
        if (!$this-&gt;connection) {
            $this-&gt;connection = Core::getConnection();
        }
    }
}

// instantiation would be something like -- given you have $connection already instantiated
$connection =  \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);

$service = new \Blog\Application\DefaultLeadRecordService(
    new \Blog\Infrastructure\DbalLeadQueryService($connection),
    new \Blog\Infrastructure\DbalLeadCommandHandler($connection),
);

// if we allow construct get default value
$service = new \Blog\Application\DefaultLeadRecordService(
    new \Blog\Infrastructure\DbalLeadQueryService(),
    new \Blog\Infrastructure\DbalLeadCommandHandler(),
);</code></pre>
<p>While on PHP 8.1, you will be able to write it like so:</p>
<pre><code class="language-php">class DefaultLeadRecordService implements LeadRecordService
{
    public function __construct(
        private LeadQueryService $queryService = new DbalLeadQueryService(),
        private LeadCommandHandler $commandHandler = new DbalLeadCommandHandler()
    ) {
    }
}

// we see there is still room for new features here
// still not that great
class DbalLeadQueryService implements LeadQueryService
{
    public function __construct(private ?Connection $connection = null)
    {
        // waiting when <code>new initializers</code> feature allows static function as default parameters
        if (!$this-&gt;connection) {
            $this-&gt;connection = Core::getConnection();
        }
    }
}

$service = new \Blog\Application\DefaultLeadRecordService();</code></pre>
<p>One liner! That saves a lot of typing, and the code remains very well structured. This is the type of significant impact we will have on our day-to-day work. We will write a more straightforward, robust and meaningful code, and we will ship features faster with high-quality code.</p>
<p>If you want to see a full implementation of this, check the code at <a href="https://github.com/rafaelbernard/blog-php-81-new-initializers">https://github.com/rafaelbernard/blog-php-81-new-initializers</a></p>
<h2>Test, our faithful friend</h2>
<p>Writing tests is a must-have for any repository where quality is a requirement. However, the &quot;New in initializers&quot; feature does not force us to give up on a complete suite of tests. We still have all powers of unit or integration tests.</p>
<p>For application code, we would write unit tests and all the expectations for concrete dependencies:</p>
<pre><code class="language-php">&lt;?php

namespace Test\Unit\Blog\Application;

use Blog\Application\DefaultLeadRecordService;
use Blog\Domain\LeadCommandHandler;
use Blog\Domain\LeadQueryService;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

class DefaultLeadRecordServiceTest extends TestCase
{
    private const EMAIL = &#039;fake@email.com&#039;;

    private LeadQueryService|MockObject $leadQueryServiceMock;
    private LeadCommandHandler|MockObject $leadCommandHandlerMock;

    private DefaultLeadRecordService $service;

    protected function setUp(): void
    {
        parent::setUp();

        $this-&gt;leadQueryServiceMock = $this-&gt;getMockBuilder(LeadQueryService::class)-&gt;getMock();
        $this-&gt;leadCommandHandlerMock = $this-&gt;getMockBuilder(LeadCommandHandler::class)-&gt;getMock();

        $this-&gt;service = new DefaultLeadRecordService($this-&gt;leadQueryServiceMock, $this-&gt;leadCommandHandlerMock);
    }

    public function testCanAdd()
    {
        $this-&gt;leadQueryServiceMock
            -&gt;expects(self::once())
            -&gt;method(&#039;getByEmail&#039;)
            -&gt;with(self::EMAIL)
            -&gt;willReturn(false);

        $this-&gt;leadCommandHandlerMock
            -&gt;expects(self::once())
            -&gt;method(&#039;add&#039;)
            -&gt;with(self::EMAIL)
            -&gt;willReturn(1);

        $result = $this-&gt;service-&gt;add(self::EMAIL);

        self::assertEquals(1, $result);
    }

    public function testAddExistentReturnsFalse()
    {
        $this-&gt;leadQueryServiceMock
            -&gt;expects(self::once())
            -&gt;method(&#039;getByEmail&#039;)
            -&gt;with(self::EMAIL)
            -&gt;willReturn([&#039;email&#039; =&gt; self::EMAIL]);

        $this-&gt;leadCommandHandlerMock
            -&gt;expects(self::never())
            -&gt;method(&#039;add&#039;);

        $result = $this-&gt;service-&gt;add(self::EMAIL);

        self::assertFalse($result);
    }

    public function testCanGetAll()
    {
        $unsorted = [
            [&#039;email&#039; =&gt; &#039;z@email.com&#039;],
            [&#039;email&#039; =&gt; &#039;a@email.com&#039;],
            [&#039;email&#039; =&gt; &#039;b@email.com&#039;],
        ];

        $this-&gt;leadQueryServiceMock
            -&gt;expects(self::once())
            -&gt;method(&#039;getAll&#039;)
            -&gt;willReturn($unsorted);

        $fetched = $this-&gt;service-&gt;getAll();

        $expected = $unsorted;
        asort($expected);

        self::assertEquals($expected, $fetched);
    }
}
</code></pre>
<p>Integration tests can be written for infrastructure code. For instance, we can use an SQLite database file to assert the logic for database operations.</p>
<p>Be aware that I am creating an SQLite temp database file on-demand for each test execution with <code>$this-&gt;databaseFilePath = &#039;/tmp/test-&#039; . time();</code> and, thanks to the Dbal library, we can be confident that operations could work for any database.</p>
<p>-&gt; <em>It is highly recommended that, as an alternative, create a container with a seeded database that is compatible with your production database system.</em></p>
<pre><code class="language-php">&lt;?php

namespace Test\Integration\Blog\Infrastructure;

use Blog\Infrastructure\DbalLeadQueryService;
use Doctrine\DBAL\Connection;
use Faker\Factory;
use Faker\Generator;
use Test\TestCase;

class DbalLeadQueryServiceTest extends TestCase
{
    private string $databaseFilePath;

    private Generator $faker;
    private Connection $connection;

    private DbalLeadQueryService $service;

    public function testCanGetAll()
    {
        $this-&gt;addEmail($email1 = $this-&gt;faker-&gt;email());
        $this-&gt;addEmail($email2 = $this-&gt;faker-&gt;email());
        $this-&gt;addEmail($email3 = $this-&gt;faker-&gt;email());

        $expected = [
            [&#039;email&#039; =&gt; $email1],
            [&#039;email&#039; =&gt; $email2],
            [&#039;email&#039; =&gt; $email3],
        ];

        $fetched = $this-&gt;service-&gt;getAll();

        self::assertEquals($expected, $fetched);
    }

    protected function setUp(): void
    {
        parent::setUp();

        $this-&gt;faker = Factory::create();

        $this-&gt;createLeadTable();

        $this-&gt;service = new DbalLeadQueryService($this-&gt;connection());
    }

    protected function tearDown(): void
    {
        parent::tearDown();

        $this-&gt;dropDatabase();
    }

    private function connection(): Connection
    {
        if (!isset($this-&gt;connection)) {
            $this-&gt;databaseFilePath = &#039;/tmp/test-&#039; . time();

            $config = new \Doctrine\DBAL\Configuration();
            $connectionParams = [
                &#039;url&#039; =&gt; &quot;sqlite:///{$this-&gt;databaseFilePath}&quot;,
            ];

            $this-&gt;connection = DriverManager::getConnection($connectionParams, $config);
        }

        return $this-&gt;connection;
    }

    private function dropDatabase()
    {
        @unlink($this-&gt;databaseFilePath);
    }

    private function createLeadTable(): void
    {
        $this-&gt;connection()-&gt;executeQuery(&#039;CREATE TABLE IF NOT EXISTS leads ( email VARCHAR )&#039;);
    }

    private function addEmail(string $email): int
    {
        return $this-&gt;connection()-&gt;insert(&#039;leads&#039;, [&#039;email&#039; =&gt; $email]);
    }
}
</code></pre>
<h2>Conclusion</h2>
<p>PHP is evolving very quickly, with new features that enable more quality software, help developers and is even more committed to the fact that most of the web run flavours of PHP code. New features improve readability, software architecture, test coverage and performance. Those are all proof of a mature and live language.</p>
<p>Upgrade to PHP 8.1 and use &quot;new in initializers&quot; as soon as possible. You will not regret it.</p>
<p>If there is something you want to discuss more, let me know in the comments.</p>
<h3>Links:</h3>
<ol>
<li><a href="https://wiki.php.net/rfc/new_in_initializers">New in initializers RFC</a></li>
<li><a href="https://web.archive.org/web/20231004075650/https://road-to-php.com/php-81">Road to PHP 8.1</a></li>
<li><a href="https://en.wikipedia.org/wiki/Dependency_injection">Dependency Injection</a></li>
<li><a href="https://en.wikipedia.org/wiki/Dependency_inversion_principle">Dependency inversion principle</a></li>
<li><a href="https://en.wikipedia.org/wiki/Interface_segregation_principle">Interface segregation principle</a></li>
<li><a href="http://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html">Solid relevance</a></li>
<li><a href="https://en.wikipedia.org/wiki/SOLID">SOLID principles</a></li>
</ol>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/php-8-1-more-on-new-in-initializers.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1456</post-id>	</item>
		<item>
		<title>Tropeçando 103</title>
		<link>https://rafael.bernard-araujo.com/tropecando-103.php</link>
					<comments>https://rafael.bernard-araujo.com/tropecando-103.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Thu, 28 Oct 2021 02:40:48 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Tropeçando]]></category>
		<category><![CDATA[ddd]]></category>
		<category><![CDATA[domain-driven design]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[software engineering]]></category>
		<category><![CDATA[test]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=1425</guid>

					<description><![CDATA[What is Domain-Driven Design (DDD) A definition of DDD as a software design discipline How to refactor without overtime and missed deadlines A lot of software engineers, including myself, are passionate about code quality. This striving for a well-shaped codebase, while getting things done could cost one quite a few hours and nerves, though. I'm [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><a href="https://verraes.net/2021/09/what-is-domain-driven-design-ddd/">What is Domain-Driven Design (DDD)</a></p>
<blockquote><p>
A definition of DDD as a software design discipline
</p></blockquote>
<p><a href="https://dev.to/s2engineers/how-to-refactor-without-overtime-and-missed-deadlines-351e">How to refactor without overtime and missed deadlines</a></p>
<blockquote><p>
A lot of software engineers, including myself, are passionate about code quality. This striving for a well-shaped codebase, while getting things done could cost one quite a few hours and nerves, though. I'm constantly looking for ways to achieve these two goals without significant trade-offs. Stand by for the current state.
</p></blockquote>
<p><a href="https://tsh.io/blog/php-unit-testing/">How to test a PHP app? PHP unit testing and more</a></p>
<blockquote><p>
Do you really need to create tests? Of course, there are many reasons to do so – improved quality of software, decreased risks while making changes in the code, identifying errors, checking business requirements, improving security…I could go on and on with that. The point is –  tests do make a difference.
</p></blockquote>
<p><a href="https://moduscreate.com/blog/application-modernization-isnt-just-fighting-legacy-tech/">Application Modernization Isn’t Just Fighting Legacy Tech</a></p>
<blockquote><p>
When radical innovations were rare, businesses could afford to treat application modernization as a sporadic reaction to change. A decade ago, most organizations modernized only when they were compelled to.</p>
<p>However, in the era of open-source and continuous innovation, modernization can’t be an isolated, one-off project. Businesses need to embrace a culture that celebrates change to thrive in the digital age. According to a report by F5, the past year has witnessed 133% growth in application modernization.
</p></blockquote>
<p><a href="https://www.thoughtworks.com/about-us/social-change/responsible-tech-playbook">Responsible tech playbook</a></p>
<blockquote><p>
As technology becomes more central to peoples' lives, and to what businesses do, and how they succeed, the ethics of technology must come into sharper focus.</p>
<p>Despite technology becoming a critical part of what enterprizes do, it's not always clear how to approach and apply technology in an ethical or responsible way. </p>
<p>The Responsible tech playbook is a collection of tools, methods, and frameworks that help you to assess, model and mitigate values and risks of the software you are creating with a special emphasis on the impact of your work on the individual and society.
</p></blockquote>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/tropecando-103.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1425</post-id>	</item>
		<item>
		<title>PHP Memory Usage and Performance Improvements Tips</title>
		<link>https://rafael.bernard-araujo.com/php-memory-usage-and-performance-improvements-tips.php</link>
					<comments>https://rafael.bernard-araujo.com/php-memory-usage-and-performance-improvements-tips.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Mon, 06 Sep 2021 16:55:25 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[performance]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[software engineering]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=1415</guid>

					<description><![CDATA[Memory usage and performance improvements make everybody happier, from end-user to cloud and infrastructure engineers. And they are all right, and this is an optimization that we should try to achieve as much as possible. I am also keeping this page for a reference to my future self because we cannot rely too much on [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>Memory usage and performance improvements make everybody happier, from end-user to cloud and infrastructure engineers. And they are all right, and this is an optimization that we should try to achieve as much as possible.</p>
<p>I am also keeping this page for a reference to my future self because we cannot rely too much on our memory, and that will be a good reference I want to re-visit. I will make constant updates on this page.</p>
<h2></h2>
<h2><a href="https://www.slideshare.net/nikita_ppv/php-performance-trivia/31">Use objects with declared properties over array</a></h2>
<p>Arrays have a larger footprint to avoid constant memory pointers reassignments. It then reserves large amounts of memory when more elements or indexes are added.</p>
<p><img data-recalc-dims="1" height="150" width="150" decoding="async" src="https://i0.wp.com/rafael.bernard-araujo.com/wp-content/uploads/2021/09/php-performance-trivia-32-1024.webp?resize=150%2C150&#038;ssl=1" alt="Image for array vs object memory usage" /></p>
<h2></h2>
<h2><a href="https://www.slideshare.net/nikita_ppv/php-performance-trivia/39">Be careful to self-referencing that would prevent garbage collector from work</a></h2>
<p>Garbage collector is working as expected when the internal reference count (how may times a value is used) reaches zero:</p>
<pre><code class="language-php">$x = "foobar";    // refcount = 1
$y = $x;            // refcount = 2
unset($x);      // refcount = 1
unset($y);      // refcount = 0 -&gt; garbage collector will be happy ==&gt; Destroy!</code></pre>
<p>But self-referencing can be tricky:</p>
<pre><code class="language-php">$x = [];            // refcount = 1
$x[0] =&amp; $x;    // refcount = 2
unset($x);      // refcount = 1
                    // It will never come to zero due to cycle</code></pre>
<p>The cycle collector will eventually destroy it, but it will hang on memory for a while anyway.</p>
<h2>Sprintf vs double/single quote concatenation</h2>
<p>A very common use case is string concatenation or interpolation when you want to add a variable into a static string. It is interesting to note that:</p>
<p>If you have PHP &lt; 7.4, use double-quote interpolation or single quote concatenation over <code>sprintf</code> function.</p>
<pre><code class="language-php">&lt;?php 

$this-&gt;start($loop);

ob_start();

for ($i = 0; $i &lt; $this-&gt;loop; ++$i) {
    print &#039;Lorem &#039;.$i.&#039; ipsum dolor sit amet, consectetur adipiscing elit. Proin malesuada, nisl sit amet congue blandit&#039;;
}

ob_end_clean();

return $this-&gt;end();</code></pre>
<p>If you have PHP greater than 7.4, use <code>sprintf</code>:</p>
<pre><code class="language-php">&lt;?php 

$this-&gt;start($loop);

for ($i = 0; $i &lt; $this-&gt;loop; ++$i) {
    $value = sprintf(&#039;Lorem %s ipsum dolor sit amet, consectetur adipiscing elit. Proin malesuada, nisl sit amet congue blandit&#039;, $i);
}

return $this-&gt;end();</code></pre>
<ul>
<li><a href="https://php.lito.com.es/test/compare/SprintfTest/PrintSingleQuoteCodeConcatTest">https://php.lito.com.es/test/compare/SprintfTest/PrintSingleQuoteCodeConcatTest</a></li>
<li><a href="https://php.lito.com.es/test/compare/SprintfTest/PrintDoubleQuoteCodeTest">https://php.lito.com.es/test/compare/SprintfTest/PrintDoubleQuoteCodeTest</a></li>
</ul>
<h2><a href="https://www.phpbench.com/">PHP Benchmarking</a></h2>
<blockquote><p>PHPBench.com was constructed as a way to open people's eyes to the fact that not every PHP code snippet will run at the same speed. You may be surprised at the results that this page generates, but that is ok. This page was also created so that you would be able to find discovery in these statistics and then maybe re-run these tests in your own server environment to play around with this idea yourself, by using the code examples (these code examples are automatically generated and as the code in my .php files change, so do they).</p></blockquote>
<h2></h2>
<h2><a href="https://php.lito.com.es/">PHP benchmarks and optimizations</a></h2>
<blockquote><p>Collection of tests and benchmarks for common operations in PHP. Tests run on several versions of PHP. There is an option to compare different solutions for the same problem to compare performances between them, such as checking values with <code>isset</code> against <code>!empty</code>.</p></blockquote>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/php-memory-usage-and-performance-improvements-tips.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1415</post-id>	</item>
		<item>
		<title>PHP Test Coverage Using Bitbucket and Codacy</title>
		<link>https://rafael.bernard-araujo.com/php-test-coverage-using-bitbucket-and-codacy.php</link>
					<comments>https://rafael.bernard-araujo.com/php-test-coverage-using-bitbucket-and-codacy.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Fri, 21 Feb 2020 12:23:22 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[coverage]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[qa]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=1347</guid>

					<description><![CDATA[Wikipedia: In computer science, code coverage is a measure used to describe the degree to which the source code of a program is tested by a particular test suite. A program with high code coverage has been more thoroughly tested and has a lower chance of containing software bugs than a program with low code [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>Wikipedia:</p>
<blockquote><p>
In computer science, code coverage is a measure used to describe the degree to which the source code of a program is tested by a particular test suite. A program with high code coverage has been more thoroughly tested and has a lower chance of containing software bugs than a program with low code coverage.
</p></blockquote>
<p>Testing is an unavoidable process for building a trustful software. Unfortunately, in PHP world we have a massive number of legacy software still running today that are very valuable but born in an age where testing was skipped for various reasons.</p>
<p>As today we are refactoring those untested systems into tested ones or we are creating new projects already focusing on having tests, we can go one step further and measure the code coverage, leveraging bug protection and code quality.</p>
<p>You can use these steps for a legacy project, a new project, a well-covered project, a poorly covered project; no matter the state of your project.</p>
<p>We are considering a PHP project using Bitbucket Pipelines as our CI and Codacy for monitoring our test coverage reports but the main concepts could be easily used when using other tools.</p>
<p>Table of contents:</p>
<ol>
<li>Dependencies installation</li>
<li>PHPUnit configuration</li>
<li>Set up Codacy project API token</li>
<li>Pipelines configuration</li>
</ol>
<h1>1. Dependencies installation</h1>
<p>Use composer to install dependencies:</p>
<pre><code class="language-sh">composer require --dev phpunit/php-code-coverage codacy/coverage</code></pre>
<p>Installation results would be similar to:</p>
<pre><code class="language-sh">Using version ^1.4 for codacy/coverage
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 3 installs, 0 updates, 0 removals
  - Installing symfony/process (v5.0.4): Downloading (100%)
  - Installing gitonomy/gitlib (v1.2.0): Downloading (100%)
  - Installing codacy/coverage (1.4.3): Downloading (100%)
Writing lock file
Generating autoload files
ocramius/package-versions:  Generating version class...
ocramius/package-versions: ...done generating version class</code></pre>
<h1>2. PHPUnit configuration</h1>
<p>We need to configure at least <code>whitelist</code> and <code>logging</code> sections. They are required to code coverage.</p>
<p><strong>Whitelist</strong> is the section that determines which files will be considered as your available code and how existent tests cover this code.</p>
<p>As I want that all my code loaded and analyzes by PHPUnit, I will set <code>processUncoveredFilesFromWhitelist</code>. Considering that all my code is under <code>./src</code> folder:</p>
<pre><code class="language-xml">&lt;phpunit&gt;
    &lt;!-- ... --&gt;
    &lt;filter&gt;
        &lt;!-- ... --&gt;
        &lt;whitelist processUncoveredFilesFromWhitelist=&quot;true&quot;&gt;
            &lt;directory suffix=&quot;.php&quot;&gt;./src&lt;/directory&gt;
        &lt;/whitelist&gt;
    &lt;/filter&gt;
&lt;/phpunit&gt;</code></pre>
<p><strong>Logging</strong> is where we configure logging of the test execution. Clover configuration is enough for now:</p>
<pre><code class="language-xml">&lt;phpunit&gt;
    &lt;!-- ... --&gt;
    &lt;logging&gt;
      &lt;log type=&quot;coverage-clover&quot; target=&quot;/tmp/coverage.xml&quot;/&gt;
    &lt;/logging&gt;
&lt;/phpunit&gt;</code></pre>
<p>You should now run your tests locally to ensure that you can fix everything that will be analyzed by PHPUnit. All errors have to be fixed. One example that might appear:</p>
<pre><code class="language-sh">Fatal error: Interface &#039;InterfaceClass&#039; not found in /var/www/src/Example/Application/ClassService.php on line 5</code></pre>
<p>Oops.</p>
<pre><code class="language-php">&lt;?php

namespace Example;

class ClassService implements InterfaceClass {
    /* */
}</code></pre>
<p>I have a class extending from another but I missed the import for the parent class.</p>
<pre><code class="language-php">&lt;?php

namespace Example;

use Example\Domain\InterfaceClass;

class ClassService implements InterfaceClass {
    /* */
}</code></pre>
<p>And now I am good. After fixing all errors that might appear (and discovering some dead classes...), we test results and report generation message:</p>
<pre><code>OK (10 tests, 17 assertions)

Generating code coverage report in Clover XML format ... done [5.71 seconds]</code></pre>
<p>This has already configured code coverage. We will use Codacy as a tool to keep track of code coverage status, representing them in a beautiful dashboard and some other tools such as a check for new pull requests.</p>
<h1>3. Set up Codacy project API token</h1>
<p>For sending coverage results to Codacy, we need the <a href="https://docs.codacy.com/repositories-configure/integrations/project-api/" title="project API token">project API token</a>. This is located at <em>Settings &gt; Integrations</em> tab.</p>
<p>If there is already a code, you can use it. Otherwise, generate one. A project token would look like something as:</p>
<pre><code>a9564ebc3289b7a14551baf8ad5ec60a // not real</code></pre>
<p>We will use this as an environment variable at Bitbucket. At your project in Bitbucket, go to <em>Configurations &gt; Pipelines &gt; Repository Variables</em>. In my case, I used:</p>
<p>Name: <code>CODACY_PROJECT_TOKEN</code><br />
Value: <code>a9564ebc3289b7a14551baf8ad5ec60a</code></p>
<p>I want the value securely encrypted. Then I mark &quot;Secure&quot;.</p>
<p>Right now we have Codacy token and the value enabled to use as an environment variable at our pipeline.</p>
<h1>4. Pipelines configuration</h1>
<p>For Pipelines now you should provide API token as an environment variable:</p>
<pre><code class="language-yaml">variables:
  - CODACY_PROJECT_TOKEN: $CODACY_PROJECT_TOKEN</code></pre>
<p>Enable xdebug:</p>
<pre><code class="language-yaml">  - pecl install xdebug-2.9.2 &amp;&amp; docker-php-ext-enable xdebug</code></pre>
<p>And execute <code>codacycoverage</code> to send saved report to Codacy:</p>
<pre><code class="language-yaml">  - src/vendor/bin/codacycoverage clover /tmp/coverage.xml</code></pre>
<p>Considering one of my legacy projects that I am adding code coverage, this could be my unit test step:</p>
<ul>
<li>using alpine</li>
<li>source code (whitelisted) at <code>site/src</code></li>
<li>logfile generated at <code>/tmp/unit-clover.xml</code></li>
<li><code>g++ gcc make git php7-dev</code> are required to install and enable xdebug</li>
</ul>
<pre><code class="language-yaml">    - step: &amp;step-unit-tests
        name: unit tests
        image: php:7.2-alpine
        variables:
          - CODACY_PROJECT_TOKEN: $CODACY_PROJECT_TOKEN
        caches:
          - composer
        script:
          - apk add --no-cache g++ gcc make git php7-dev
          - pecl install xdebug-2.9.2 &amp;&amp; docker-php-ext-enable xdebug
          - site/src/vendor/bin/phpunit -c tests/Unit/phpunit.xml
          - site/src/vendor/bin/codacycoverage clover /tmp/unit-clover.xml</code></pre>
<p>After being successfully executed by pipelines, you may see the results at your Codacy dashboard.</p>
<p>Now code with love.<br />
And coverage.</p>
<hr />
<p>Useful links:</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Code_coverage">https://en.wikipedia.org/wiki/Code_coverage</a></li>
<li><a href="https://xdebug.org/">https://xdebug.org/</a></li>
<li><a href="https://docs.phpunit.de/en/11.3/code-coverage.html">https://docs.phpunit.de/en/11.3/code-coverage.html</a></li>
<li><a href="https://phpunit.readthedocs.io/en/8.5/code-coverage-analysis.html#whitelisting-files">https://phpunit.readthedocs.io/en/8.5/code-coverage-analysis.html#whitelisting-files</a></li>
<li><a href="https://phpunit.readthedocs.io/en/8.5/configuration.html#the-logging-element">https://phpunit.readthedocs.io/en/8.5/configuration.html#the-logging-element</a></li>
<li><a href="https://en.wikipedia.org/wiki/Test-driven_development">https://en.wikipedia.org/wiki/Test-driven_development</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/php-test-coverage-using-bitbucket-and-codacy.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1347</post-id>	</item>
	</channel>
</rss>
