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.
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:root@127.0.0.1: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.
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.
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 ?
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
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
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).
Note that activation is working from the api/docs in apiPlateform , what is not working is the activation via the email link.
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
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.