How to create a simple application using Symfony and React Native – Part 1

Symfony application stock portfolio

This is the first part of a new series I want to do. In this series I’ll be making a simple stock portfolio application using Symfony with API Platform. For the app we will use React Native. I’ll take you through the whole process, from setting up the basic Symfony application to the deployment of the API. You can follow along with me at Github.

Create a new Symfony project

Let’s get started! First we’ll work on our API, because the mobile app has to talk to something, right? The first step is creating a new Symfony application. We can create the application using the Symfony CLI, which I recommend because of it’s awesome features, or use the composer create-project command. Run one of the following commands in your terminal.

symfony new api composer create-project symfony/skeleton api
Code language: JavaScript (javascript)

Next I like to add a composer script to make it easy to start the development server. This does use the Symfony CLI, so if you’ve used the composer install method you can skip this step.

To add the script open your composer.json file using a text editor and add the following to the scripts section.

"scripts": { "dev": "symfony server:start --port=8000", // Other scripts },
Code language: JavaScript (javascript)

Adding this line allows you to execute composer run dev to start the development webserver on port 8000. Try it and open http://localhost:8000 in your browser. You should see something like this.

How to create a simple application using Symfony and React Native - Part 1

Add base packages to our application

Now that we’ve got our basic Symfony application running it’s time to add some extra packages into the mix. First off we need to install API Platform. To do this run the following command.

composer require api
Code language: JavaScript (javascript)

Next we’re going to add a few extra packages for logging, a package for JWT authentication and some extensions for Doctrine.

composer require symfony/monolog-bundle composer require stof/doctrine-extensions-bundle composer require jwt-auth composer require debug
Code language: JavaScript (javascript)

When the packages are added we can move on to the next step, configuring the application.

Configure the API and Security

Let’s start with the API configuration. Open the api_platform.yaml in your config/packages folder and make sure the mapping is configured. While you’re there, add a nice title and description for your API. This is what mine looks like.

api_platform: title: 'Stock Portfolio API' description: 'The wonderful API of our stock portfolio application' version: '1.0.0' enable_docs: true enable_swagger: true enable_swagger_ui: true defaults: pagination_enabled: true pagination_client_items_per_page: true pagination_items_per_page: 100 pagination_maximum_items_per_page: 250 collection: pagination: items_per_page_parameter_name: pageSize mapping: paths: ['%kernel.project_dir%/src/Entity'] patch_formats: json: ['application/merge-patch+json'] swagger: api_keys: apiKey: name: Authorization type: header versions: [3]
Code language: JavaScript (javascript)

As you can see I’ve added a lot of configuration. API Platform is highly configurable which makes it really versatile for every day usage.

In my configuration I’ve set the title, description and version of my API. I’ve also enabled the automatic documentation and Swagger UI support. I’ve setup some defaults for pagination and changing the pageSize from the client. Using the swagger entry I’ve added support for using JWT tokens from the Swagger UI, so you can test your API more easily.

Now that we’re on the subject of JWT tokens, let’s configure the JWT bundle and security of our API. First we need to generate a private and a public key. I assume you’ll running these commands under Linux or WSL.

mkdir -p config/jwt jwt_passphrase=${JWT_PASSPHRASE:-$(grep ''^JWT_PASSPHRASE='' .env | cut -f 2 -d ''='')} echo "$jwt_passphrase" | openssl genpkey -out config/jwt/private.pem -pass stdin -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096 echo "$jwt_passphrase" | openssl pkey -in config/jwt/private.pem -passin stdin -out config/jwt/public.pem -pubout
Code language: PHP (php)

This will generate the necessary private and public keys for us in the config/jwt directory. These keys will be used by the JWT Bundle to generate and -most importantly- validate the JWT tokens.

Now we need to tell Symfony to use the JWT Bundle to authenticate our users. Let’s edit config/packages/security.yaml to do this!

security: # Enable the new authenticator manager of Symfony enable_authenticator_manager: true # Set the password hasher we'll use for securing the passwords password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # Register a doctrine user provider providers: app_user_provider: entity: class: App\Entity\User property: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false # Define our API firewall and configure it to use JWT for authentication api: pattern: ^/api/ stateless: true provider: app_user_provider jwt: ~ # Define our main firewall and configure it to use the JSON login method on the /token endpoint main: json_login: check_path: /token username_path: email password_path: password success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure # Define some access control rules access_control: - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api/docs, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api, roles: ROLE_USER }

Now we need to update our routes as we’ve defined our API to be served from /api/ and we need to register the /token endpoint. Open routes/api_platform.yaml and make sure the prefix is set to /api like this.

api_platform: resource: . type: api_platform prefix: /api

Next lets add our /token url to the list of routes. Open config/routes.yaml and add the following.

token: path: /token methods: ['POST']
Code language: PHP (php)

That makes sure Symfony knows about the /token route and the Security component will automatically intercept calls to this route and send the request to our JWT Bundle.

Configure the rest

Database

Lets start by configuring the database. This configuration is done using environment variables, so lets make this happen. First we’ll create a copy of our .env file and name it .env.local which should not be tracked by git. So make sure .env.local is in the .gitignore file. Next we find the database configuration which starts with DATABASE_URL. In my case it’s uncommented and adds a connection to a Postgres database. Because I’m using a MariaDB database server I’ll change it to reflect my connection.

DATABASE_URL="mysql://root:[email protected]:3306/stock_portfolio?serverVersion=mariadb-10.5.12"
Code language: JavaScript (javascript)

This line makes sure we’ll connect to the MariaDB server running on my machine using root as the user and the password and we’ll call our database stock_portfolio. If this database does not exist you can have Symfony create it for you. Simply run this command.

bin/console doctrine:database:create
Code language: JavaScript (javascript)

This will create your database for you.

Doctrine Extensions

Because we want to use the Doctrine Extensions bundle we’ll need to configure Doctrine to use the extensions. Luckily this is pretty easy to do. In my case we’ll want to use the Timestampable and the SoftDeletable functionality. To make this happen we’ll need to enable the extensions by editing the config/packages/stof_doctrine_extensions.yaml file.

stof_doctrine_extensions: default_locale: en_US orm: default: timestampable: true softdeleteable: true
Code language: JavaScript (javascript)

Because we want to use the SoftDeletable functionality we’ll also need to register a Doctrine filter by editing the config/packages/doctrine.yaml file under the orm key.

doctrine: dbal: url: '%env(resolve:DATABASE_URL)%' orm: auto_generate_proxy_classes: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true filters: softdeleteable: class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter enabled: true mappings: App: is_bundle: false type: attribute # Needs to be set to attribute for PHP 8 attributes to work dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App

We’ve added the softdeletable filter to the orm entry. This will make sure the filter is used on the default EntityManager of Doctrine.

In part 2 we’ll create the user register endpoint where new users can create an account.

8 comments

  1. Hi, maybe you could help me.

    I cannot register a user as i encountered an issue with the token.

    “hydra:description”: “An error occurred while trying to encode the JWT token. Please verify your configuration (private key/passphrase)”,

    I tried everything possible:
    regenerate keys, changing, passphrase in both .env or putting clear passphrase in yaml lexik configuraiton file.
    I tried to execute the lexik command to generate keys

    My keys are generated but i always face this issue now for 2 days i’m stuck.

    PS: sorry for my english i hope you unerstand.

  2. Hi,
    I managed to make it work so my comment is useless now.

    But i’m facing another issue when i try to activate account from email (button or link)

    I have a no route found error for the activation.

    Can you help me ?

    1. Hi,

      I’m glad you managed it. Sorry it took so long for me to reply!

      What’s the output when you run
      bin/console debug:router

      Does it list the activation endpoint? If not you’ll need to check your routing.yml file and check if API Platform is listed there. It’s also possible you have an error in your entity configuration. Check that aswell.

      With regards,
      Wouter

  3. Hi ,

    Thanks for the reply.

    Here is the output of the debug:router command:

    api_entrypoint ANY ANY ANY /api/{index}.{_format}
    api_doc ANY ANY ANY /api/docs.{_format}
    api_jsonld_context ANY ANY ANY /api/contexts/{shortName}.{_format}
    api_portfolios_get_collection GET ANY ANY /api/portfolios.{_format}
    api_portfolios_post_collection POST ANY ANY /api/portfolios.{_format}
    api_portfolios_get_item GET ANY ANY /api/portfolios/{id}.{_format}
    api_portfolios_delete_item DELETE ANY ANY /api/portfolios/{id}.{_format}
    api_portfolios_put_item PUT ANY ANY /api/portfolios/{id}.{_format}
    api_portfolios_patch_item PATCH ANY ANY /api/portfolios/{id}.{_format}
    api_stocks_get_collection GET ANY ANY /api/stocks.{_format}
    api_stocks_post_collection POST ANY ANY /api/stocks.{_format}
    api_stocks_get_item GET ANY ANY /api/stocks/{id}.{_format}
    api_stocks_delete_item DELETE ANY ANY /api/stocks/{id}.{_format}
    api_stocks_put_item PUT ANY ANY /api/stocks/{id}.{_format}
    api_stocks_patch_item PATCH ANY ANY /api/stocks/{id}.{_format}
    api_users_post_collection POST ANY ANY /api/users.{_format}
    api_users_register_collection POST ANY ANY /api/public/users/register
    api_users_activate_collection POST ANY ANY /api/public/users/activate
    api_users_get_item GET ANY ANY /api/users/{id}.{_format}
    api_users_put_item PUT ANY ANY /api/users/{id}.{_format}
    api_users_delete_item DELETE ANY ANY /api/users/{id}.{_format}
    _preview_error ANY ANY ANY /_error/{code}.{_format}
    _wdt ANY ANY ANY /_wdt/{token}
    _profiler_home ANY ANY ANY /_profiler/
    _profiler_search ANY ANY ANY /_profiler/search
    _profiler_search_bar ANY ANY ANY /_profiler/search_bar
    _profiler_phpinfo ANY ANY ANY /_profiler/phpinfo
    _profiler_search_results ANY ANY ANY /_profiler/{token}/search/results
    _profiler_open_file ANY ANY ANY /_profiler/open
    _profiler ANY ANY ANY /_profiler/{token}
    _profiler_router ANY ANY ANY /_profiler/{token}/router
    _profiler_exception ANY ANY ANY /_profiler/{token}/exception
    _profiler_exception_css ANY ANY ANY /_profiler/{token}/exception.css
    token POST ANY ANY /token

  4. As you can see in my comment above, the route
    api_users_activate_collection POST ANY ANY /api/public/users/activate is here.

    But what i don’t understand is why in the UserService we have this:

    ‘activationLink’ => sprintf(‘%sactivate-account?token=%s’, $this->frontendUrl, $token),

    with is not he same path as the user activation in the API .

    I think i misunderstood something, but to me the route for this path doesn’t not exist (so the error “NO ROUTE FOUND” is normal);

    The other thing i can’t understand is that the API route is a POST route, so event in i change this line:
    ‘activationLink’ => sprintf(‘%sactivate-account?token=%s’, $this->frontendUrl, $token),

    to :

    ‘activationLink’ => sprintf(‘%sapi/public/users/activate?token=%s’, $this->frontendUrl, $token),

    it is not working obviously because it is a GET request.

    So now i’m stuck checking your github files to see i forgot something with no chance.

    Thank you for the great job (sign in an register from my app is working well).

    1. Note that activation is working from the api/docs in apiPlateform , what is not working is the activation via the email link.

      1. Hi,

        The activationlink in the email message is the link to the account activation lage of your webapp or phone app.

        This page should perform the post request using the url of the API and show the result of this API call to the user. For example ‘Your account is activated’ or ‘The activation token your specified is invalid’.

        So in short. The URL of the activation endpoint should only be called from your frontend.

        I hope this makes it a bit clearer for you.

        With regards,
        Wouter

  5. hi, thanks !

    i just realize my misunderstaning before cheking your answer ! English is a bit difficult for me but now it’s ok .

    Many thanks for your great job. I faced other issues that i managed to resolve by myself as i’m using sf 5.4.9.

    Thank you for your patience and your explanations. I hope you will continue this serie.

Leave a Reply

Your email address will not be published.