Serializer – array to Object

To convert from an array (including multi-dimensional arrays) to an object, the following code might help!

Ref; https://symfony.com/doc/current/components/serializer.html

// all callback parameters are optional (you can omit the ones you don't use)
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = new ObjectNormalizer(null, null, null, $extractor);
$serializer = new Serializer(
    [
        $normalizer,
        new ArrayDenormalizer(),
    ]
);

$obj = $serializer->denormalize(
    $data,
    Member::class,
    null,
    [ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true]
);

Symfony

This can also be achieved by installing the following packages, which Symfony will pickup & use with it’s serializer;

composer require phpdocumentor/reflection-docblock
composer require symfony/property-info

Then, in your service (or controller);

public function __construct(SerializerInterface $serializer) {
    $this->serializer = $serializer;
}

And in your code;

$obj = $this->serializer->denormalize(
    $rowData,
    Member::class
);

You can also lessen the enforcement of variable-types (strings vs ints) with the following;

$obj = $serializer->denormalize(
    $data,
    Member::class,
    null,
    [ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true]
);

Ref; https://symfony.com/doc/current/components/property_info.html

Including the git tag as an environment var in AWS Lambda (via Bitbucket Pipeline Deployments & serverless)

When deploying with the Serverless framework (which Bitbucket Pipelines can do), I wanted to include a version number (or other vars & options passed in the Serverless CLI) which triggered the deploy (via Bitbucket Pipelines).

In my case, this is shown in the footer of a Symfony web-app (more on that below).

Here’s how this can be achieved;

Serverless

In serverless.yml, we need to define our env-var within the function (or as i’ve done, for all functions, by placing it in the ‘provider’ -> ‘environment’ variables);

DEPLOY_VERSION: ${opt:deploy-version, 'unknown'}

In the above example, my ENV file will be called ‘DEPLOY_VERSION’

The ‘${opt:…} basically gets an option we’ve specified in the serverless deploy command-line (eg. serverless deploy –deploy-version v1.2.3 )

This allows us to pass environment vars from the command line, to our functions (in our case, we’re saying version 1.2.3 of our software is getting deployed).

Then, in Bitbucket;

Next, in our bitbucket-pipelines.yml file, we need to include some extra vars in the ‘atlassian/serverless-deploy:…’ pipe – eg;

EXTRA_ARGS: '... --deploy-version $BITBUCKET_TAG'

Here, we just specify our own option called ‘deploy-version’ (eg. ‘–deploy-version’), and used a variable which bitbucket includes at deploy-time (in our case, it’s called BITBUCKET_TAG).

In my case, i’m using tags to deploy new version of an app (eg. v1.2.3)

Using it with Symfony

From there, it’s upto you how your AWS Lambda function actually uses the environment variable. In my case, i’m using Symfony (with Bref to run it on Lambda). This requires an additional couple of steps;

In the .env file, I need to specify my default value for the env file (eg. when i’m developing it locally, etc);

DEPLOY_VERSION=dev-master

From there, in my case I then include it as a global variable in my templates, by adding it to my ‘config/packages/twig.yaml‘ file;

parameters:
    deploy_version: '%env(DEPLOY_VERSION)%'

twig:
    globals:
        deploy_version: '%deploy_version%'

And then in the footer of my pages, I can include it (eg. base.twig.html);

<p><small>Version: {{ deploy_version }}</small></p>

Done!

In summary, now when we deploy via Bitbucket Pipelines, we’ll have the version number used in the tag, included in our Symfony app (or whatever Lambda function you have).

Of course this could be used for any variable available in Bitbucket Pipelines (or event via the command-line in the Serverless framework)

Enjoy!

Symfony with Bref (and AWS Lambda)

Bref is a serverless framework allowing you to use AWS Lambda with PHP sites, including Symfony apps.

In the serverless.yml file, include the following to allow the headers from the API Gateway to be picked up (eg. so Symfony knows you’re using https rather than http … and makes your absolute URLs with https accordingly);

provider:
  environment:
    TRUSTED_PROXIES: 127.0.0.1,127.0.0.2

Ref; https://symfony.com/doc/current/deployment/proxies.html

As part of this, some optimisations need to be made to the php.ini file, to get Symfony running a little faster;

; maximum memory that OPcache can use to store compiled PHP files
opcache.memory_consumption=256

; maximum number of files that can be stored in the cache
opcache.max_accelerated_files=20000

; don't check timestamps for php files in cache (comment out if php files are getting edited on the server)
; needs a clear-cache script prepared
opcache.validate_timestamps=0

; maximum memory allocated to store the results
realpath_cache_size=4096K

; save the results for 10 minutes (600 seconds)
realpath_cache_ttl=600

Ref; https://symfony.com/doc/current/performance.html

Using AWS Cognito for authentication on your app

The aim here is to use AWS Cognito to authenticate users on your Symfony app, using oAuth2 so all the auth happens externally on AWS Cognito.

I’m not storing user data locally with this — it just makes sure that they’re valid users. Groups functionality would need to be added separately if required (it’s referenced in the MyBuilder article and has instructions on how groups can be obtained).

I’ve used the knpuniversity bundle as I tried to get HWI OAuth package to work but there wasn’t any providers already written up to support Cognito.

Composer packages used;

  • knpuniversity/oauth2-client-bundle
  • cakedc/oauth2-cognito

So – to get it going, follow the KNPUniversity instructions on their git-hub account.

From there, use the following for the Authenticator class;

<?php

namespace App\Security;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2Client;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class CognitoAuthenticator extends SocialAuthenticator
{
    private $clientRegistry;
    private $router;

    public function __construct(ClientRegistry $clientRegistry, RouterInterface $router)
    {
        $this->clientRegistry = $clientRegistry;
        $this->router = $router;
    }

    public function supports(Request $request)
    {
        // continue ONLY if the current ROUTE matches the check ROUTE
        return $request->attributes->get('_route') === 'connect_cognito_check';
    }

    public function getCredentials(Request $request)
    {
        // this method is only called if supports() returns true
        return $this->fetchAccessToken($this->getClient());
    }

    /**
     * @return OAuth2Client
     */
    private function getClient()
    {
        return $this->clientRegistry
            ->getClient('cognito');
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        return $userProvider->loadUserByUsername($this->getClient()->fetchUserFromToken($credentials)->getId());
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        $targetUrl = $this->router->generate('app_default_index');

        return new RedirectResponse($targetUrl);

        // or, on success, let the request continue to be handled by the controller
        //return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }

    /**
     * Called when authentication is needed, but it's not sent.
     * This redirects to the 'login'.
     *
     * @param Request $request
     * @param AuthenticationException|null $authException
     * @return RedirectResponse
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new RedirectResponse(
            $this->router->generate('connect_cognito_start'),
            // might be the site, where users choose their oauth provider
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }
}

My ‘SecurityCognitoController’ class looks like this;

<?php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class SecurityCognitoController extends AbstractController
{
    /**
     * Link to this controller to start the "connect" process
     *
     * @Route("/security/connect-cognito", name="connect_cognito_start")
     */
    public function connectAction(ClientRegistry $clientRegistry)
    {
        // will redirect to AWS Cognito!
        return $clientRegistry
            ->getClient('cognito') // key used in config/packages/knpu_oauth2_client.yaml
            ->redirect();
    }

    /**
     * After going to Facebook, you're redirected back here
     * because this is the "redirect_route" you configured
     * in config/packages/knpu_oauth2_client.yaml
     *
     * @Route("/security/cognito/check", name="connect_cognito_check")
     */
    public function connectCheckAction(Request $request, ClientRegistry $clientRegistry)
    {
        // ** if you want to *authenticate* the user, then
        // leave this method blank and create a Guard authenticator
        
    }
}

My security.yaml file looks something like this;

security:
    providers:
        oauth:
            id: knpu.oauth2.user_provider

    firewalls:
        main:
            anonymous: true
            guard:
                authenticators:
                    - App\Security\CognitoAuthenticator
    access_control:
        - { path: ^/security/connect-cognito, roles: IS_AUTHENTICATED_ANONYMOUSLY }

And the following config was used in the ‘pnpu_oauth2_client.yaml’ file;

knpu_oauth2_client:
    clients:
        # configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
        cognito:
            type:       'generic'
            provider_class:       '\CakeDC\OAuth2\Client\Provider\Cognito'
            client_id:  '<your client id here>'
            client_secret: '<your client secret here>'
            redirect_route: connect_cognito_check
            provider_options:
                region: <your region here>
                cognitoDomain: <your cognito domain here>
                scope: 'email'

A few things are above;

  • client id; This is found in the ‘App integration’ -> ‘App client settings’ page
  • client_secret: This is in the ‘General settings’ -> ‘App clients’ page, and generated when the ‘app client’ is added
  • region: the region your Cognito user-pool is in
  • scope: I’ve just used email, but if you want you can expand it to capture other information as well.
  • redirect_route: In my case I set it to ‘connect_cognito_check’ … this endpoint is used to receive the ‘OK’ from AWS Cognito that your user has been authenticated and pass back a code which internally is used to retrieve the actual account info of the person which was authenticated.

In AWS Cognito, in your ‘App client’ you’ve setup, make sure you have the following settings;

In AWS Cognito, in your ‘App client’ you’ve setup, make sure you have the following settings;

In this case, the ‘Authorization code grant’ is required as part of the oAuth process
The scopes ticked here are what you’ve specified in the config above.
The sign-in & out URLs should be roughly like the above. (obviously this is for dev … use a prod url for your prod environment.
The callback URLs should match your ‘check’ url

Thanks to the following sites which helped get this far;

https://tech.mybuilder.com/managing-authentication-in-your-symfony-project-with-aws-cognito/
https://github.com/knpuniversity/oauth2-client-bundle
https://github.com/CakeDC/oauth2-cognito

Using Symfony on Lambda

For those going down the server-less route and using Symfony, this will hopefully give you a decent starting point.

I’ve added a ‘part 2‘ to this post, with performance optimisations, as well as handling being behind the API Gateway.

This uses a project called Bref – their website is a great starting point and has loads of info.

Ingredients (what you’ll need on-hand);

Method (how do we do this);

A lot of these instructions come from https://bref.sh

For this example, we’ll use ap-southeast-2 as the region (as all my scripts are written using it)

IMPORTANT: If you decide to use a different region (eg. one closer to home/your users), make sure you use the same region for S3 as well as the runtime (mentioned later). Otherwise you’ll run into permission issues you’ll never be able to solve!

2) Jump into the project dir (eg. my-project), and install bref using composer;

1) Install the Symfony skeleton project (see https://symfony.com/doc/current/setup.html) & check to make sure it works as expected.

composer require mnapoli/bref

You may also need to run the following;

vendor/bin/bref init

3) Create an AWS S3 bucket to store your ‘packaged’ files;

aws s3 mb s3://symfony-lambda --region=ap-southeast-2

4) Create a deploy.sh executable file to save yourself some typing;

#!/bin/bash

# Package up your files and send it to an S3 bucket you're going to use;
sam package --output-template-file .stack-symfony.yaml     --s3-bucket symfony-lambda --region=ap-southeast-2

# Deploy (using cloud-formation, which will create your lambda function, etc)
sam deploy --template-file .stack-symfony.yaml --stack-name symfony-lambda --capabilities CAPABILITY_IAM --region=ap-southeast-2

5) Create a cloudformation template file (we’ll call this ‘.stack-symfony.yaml’ … if you want to call it something different, put in the new name in the above command instead);

 AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31


Description: ''
Outputs:
  DemoHttpApi:
    Description: URL of our function in the *Prod* environment
    Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'


Globals:
    Function:
        Environment:
            Variables:
                APP_ENV: prod
Resources:
  Website:
    Properties:
      CodeUri: s3://<your s3 bucket here>/94ce2f32ac156cd3f06208d3e05dca8f
      Description: ''
      Events:
        HttpRoot:
          Properties:
            Method: ANY
            Path: /
          Type: Api
        HttpSubPaths:
          Properties:
            Method: ANY
            Path: /{proxy+}
          Type: Api
      FunctionName: symfony-website
      Handler: public/index.php
      MemorySize: 1024
      Layers:
        - arn:aws:lambda:ap-southeast-2:416566615250:layer:php-72-fpm:8
      Runtime: provided
      Timeout: 30
    Type: AWS::Serverless::Function
  MyFunctionCli:
    Properties:
      CodeUri: s3://<your s3 bucket here>/94ce2f32ac156cd3f06208d3e05dca8f
      FunctionName: symfony-console
      Handler: bin/console
      Timeout: 120  # in seconds
      Layers:
        - arn:aws:lambda:ap-southeast-2:416566615250:layer:php-72-fpm:8
        - arn:aws:lambda:ap-southeast-2:416566615250:layer:console:4
      Runtime: provided
    Type: AWS::Serverless::Function

Runtimes (basically the PHP executable which is needed by Lambda (PHP isn’t built into lambda, so we supply it as a ‘layer’). See; https://bref.sh/docs/runtimes/

In the file above, add in your s3 bucket name, and replace the ‘layers’ mentioned above with the later from your region (if you’ve decided to use a different one). See; https://bref.sh/docs/runtimes/

6) Run your ./deploy.sh script and follow the prompts if there’s any issues.

I ran into multiple permission issues which were all easily solved by giving access to the user i’d created for this project.

See the Symfony bref guide for more details if you get stuck; https://bref.sh/docs/frameworks/symfony.html

Cost;

This article is of course free, but there’s some on-going costs you’ll need to consider with Lambda. The few websites i’ve read regarding this seem to indicate it’s cheaper than running an EC2 instance (both from an actual cost and a time-cost in maintaining the thing), but of course you can do a lot of things with EC2!

The cost estimates i’m using come from US East (Ohio), and were taken on 8/Feb/2019;

  • Lambda (for your actual PHP server-side code)
    • $3.50/million requests – first 333 million requests/month
    • AWS Lambda – Compute Free Tier – 400,000 GB-Seconds – US East (Ohio)12.163 Lambda-GB-Second
    • AWS Lambda – Requests Free Tier – 1,000,000 Requests – US East (Ohio)
  • S3 (for storage of your packaged site, as well as any assets – css/images/js/uploaded files);
    • $0.005 per 1,000 PUT, COPY, POST, or LIST requests
    • $0.004 per 10,000 GET and all other requests
    • $0.023 per GB – first 50 TB / month of storage used
  • Some others include;
    • Bandwidth
    • CloudFront (for serving up your assets if you use it later on)
    • Relational Database Service (for a database if you need one)
    • Route 53 (for your DNS needs)
    • Simple Email Service (for sending emails)

Inspiration from this comes from this YouTube vid – well worth watching;

Symfony – forcing file download as attachment

The following forces the browser to download the file as an ‘attachment’, rather than inline (within the browser window);

(in this case, named ‘my-pdf-filename.pdf’)

$response = new Response($pdfData, 200, [
   'Content-Length' => strlen($pdfData),
   'Content-type' => 'application/pdf',
   'Content-Transfer-Encoding' => 'binary'
]);

$disposition = $response->headers->makeDisposition(
   ResponseHeaderBag::DISPOSITION_ATTACHMENT,
   'my-pdf-filename.pdf'
);
$response->headers->set('Content-Disposition', $disposition);

 

Symfony: Adding messages to the translator dynamically (updated)

This is an update to the previous translator post, allowing the system to use the default translator and just adding the required messages to the message-catalogue in-use.

This is a better approach as it doesn’t then mean you miss out on the good things Symfony provides out-of-the-box because you’ve had to define what specific component is in use for the translator.

Anyway — code is below;

services.yml
No changes are needed — no translator specifics are needed (apart from including a listener as in the previous post)

MyListener.php

Add a function to your kernel.event-listener as below;

/**
 * @param TranslatorInterface $translator
 */
public function addTranslatorResources(TranslatorInterface $translator) {
   $locale = $translator->getLocale();
   $catalogue = $translator->getCatalogue($locale);
   
   $translations = ['messages' => ['key1' => 'my message is here']];
   $loader = new ArrayLoader();
   $customCatalogue = $loader->load($translations, $locale);
      
   $catalogue->addCatalogue($customCatalogue);
}

Test using the following;

// debug code to test;
 echo $translator->trans('messages.key1');
References;

https://symfony.com/doc/current/translation.html

Symfony: Adding to the translator dynamically

Symfony – Validator component with Dependency Injection

If you’re using the validation/validator component (eg. https://symfony.com/doc/current/components/validator.html) outside of the symfony framework and need to access a service within a custom constraint, here’s how to do it;

Create a ContainerConstraintValidatorFactory;

If you’re using the validation/validator component outside of the Symfony framework and need to access a service within a custom constraint (eg. http://symfony.com/doc/current/validation/custom_constraint.html), here’s how to do it;

Create a ContainerConstraintValidatorFactory;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorFactory;
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;

class ContainerConstraintValidatorFactory extends ConstraintValidatorFactory implements ConstraintValidatorFactoryInterface {
 private $container;
 
 public function __construct(ContainerInterface $container) {
  parent::__construct();
  $this->container = $container;
 }
 
 public function getInstance(Constraint $constraint) {
  if($this->container->has($constraint->validatedBy())) {
   return $this->container->get($constraint->validatedBy());
  }
 
  return parent::getInstance($constraint);
 }
}

From there, when you’re creating your ‘validator’, you’ll need to use your newly created factory;

$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->setConstraintValidatorFactory(
   new ContainerConstraintValidatorFactory($container)
);
$validator = $validatorBuilder->getValidator();

This will use your container to check if the constraint-validator class is registered … if so, it’ll be used, otherwise it’ll behave as normal.

Reference: https://stackoverflow.com/questions/40601866/how-to-configure-dependencies-on-custom-validator-with-symfony-components

Symfony: Adding to the translator dynamically

UPDATE: Symfony: Adding messages to the translator dynamically (updated) contains an update to the post below

To add to the translator (eg. from the DB / by user / etc), you need to first create your own translator class or force Symfony to use the base one (otherwise it’ll compile things together and use a read-only one when it comes to accessing your site).

First, in your services.yml file;

translator:
    public: true
    alias: 'Symfony\Bundle\FrameworkBundle\Translation\Translator'

Symfony\Bundle\FrameworkBundle\Translation\Translator:
  class: Symfony\Bundle\FrameworkBundle\Translation\Translator
  arguments:
      - '@service_container'
      - '@translator.selector'
      - '%kernel.default_locale%'

This just tells Symfony to use the base translator, or alternatively you can use your own as follows;

app/config/services.yml

translator:
  public: true
  alias: 'YourNamespace\YourTranslatorClassName'

From there you can add a listener or just need to obtain an instance of the Translator on-load, and add in any resources as required – eg;

your-listener / controller / etc;

use Symfony\Component\Translation\Loader\ArrayLoader

...

$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', ["my.word" => "Hello"], $translator->getLocale());

$translator is an instance of “Symfony\Component\Translation\Translator”.

More info;
https://symfony.com/doc/current/components/translation/usage.html

Symfony – Generating absolute urls

Generating absolute urls (including hostname and scheme (http / https) is super useful for including full urls in templates or email content (eg. when you want to pass on an address to someone which is getting your emails!)

How?

In templates;

{{ absolute_url(path('route_name', {...})) }}

In controllers;

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;


$url = $this->generate(
'app_default_index',
['name' => 'Steve'], UrlGeneratorInterface::ABSOLUTE_URL
);

In services;

First import the class name for the url generator, and instance of the generator;

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/** @var UrlGeneratorInterface */
protected $urlGenerator;

/**
 * Inject URL Generator as a service
 * @param UrlGeneratorInterface $urlGenerator
 * @required
 */
public function setUrlGeneratorService(UrlGeneratorInterface $urlGenerator) {
    $this->urlGenerator = $urlGenerator;
}

Then, inside your method (in the service);

$url = $this->urlGenerator->generate(
'app_default_index',
['name' => 'Steve'], UrlGeneratorInterface::ABSOLUTE_URL
);