Symfony and Angular Full-Stack Tutorial
In this developer tutorial, learn how to build a full-stack app using Symfony and Angular. Symfony is one of the most used PHP frameworks nowadays; it is robust, has a lot of support, is easy to maintain, and has many libraries to help us during the development process. And with its MVC architecture, it is compatible with the most commonly used database engines.
Creating a RESTful API with Symfony can be really easy. For this example, we are going to use version 3.3 (one of the most frequently used) in a very basic mode. To display and manage the data on the client side, we will use Angular 4; the DB engine we’ll use is MySQL.
The example manages a simple user login, and allows us to create tasks with some states (New, TODO, Finished) related to the users.
Backend
First Step
Be sure that you have an Apache server installed on your machine. I strongly recommend a version from Apache/2.4.18 and up and PHP version 7.0 and up.
Clone this repo.
You can find the DB in the “ db” folder in the root directory; import it to your local MySQL instance.
Inside the symfony/src folder, you will find the custom code created to manage the API.
This is the folder structure:
Symfony uses the “bundle” concept, which is quite similar to “plugins” in other software. For example, the third-party features are a bundle, and in our app, the controllers, resources and services are bundles.
Going Deep
In order to reduce the number of manual tasks to be performed, you need Composer; the recommended version is 1.6.2.
While it is not the case here, if you ever want to play with a project from scratch, this command will be helpful:
$composer create-project symfony/framework-standard-edition symfony/ "version.#.#".
For our project, we will only include two specific libraries in the composer.json:
"firebase/php-jwt": "^3.0.0", //tokens authentication
"knplabs/knp-paginator-bundle": "2.5.*" // pagination
Those were added to our project using:
$composer update
Now, regarding the DB, the file that contains all the connection data is “app/config/parameters.yml”
# This file is auto-generated during the composer install
parameters:
database_host: 127.0.0.1
database_port: 8889
database_name: symfony
database_user: root
database_password: root
mailer_transport: smtp
mailer_host: 127.0.0.1
mailer_user: null
mailer_password: null
secret: ThisTokenIsNotSoSecretChangeIt
The bundle entities were generated running something like this:
php bin/console generate:bundle —namespace=BackendBundle —format=yml
Now, if you go to the BackendBundle, it is easy to find the Entity folder, which contains the entity model file with all the database table mapping. Those entity files were generated using:
$ php bin/console doctrine:mapping:import BackendBundle yml
$ php bin/console doctrine:generate:entities BackendBundle
Now that we have the models, our next step is to create actions in controllers, and mapping these actions to their entity functions. Check the AppBundle folder to get an idea of how to manage that; you will see the routing.yml in the Resources/config/routing folder. In that file, we have the instance for all the entities, and the routing folder is the place to add all the required yml. One *.yml per entity is required, in our example, we are only using:
default.yml
task.yml
user.yml
If you open any of them, you will notice that the entity functions are being related to specific URL paths. So any time that I need to add a new function, its reference needs to be created in its yml routing file.
Now, inside the src/AppBundle/Controller folder, you will have the controller files. All of them contain the functionality that gets the data from the DB, formats it and displays it as a service attached to one URL. For example, the TaskController.php includes all the basic actions to perform the CRUD in the Task entity.
public function newAction(Request $request, $id=null){
}
public function tasksAction(Request $request){
}
public function taskAction(Request $request, $id = null){
}
public function searchAction(Request $request, $search = null){
}
public function removeAction(Request $request, $id = null){
}
DefaultController contains the loginAction, and UserController contains the actions for creating a new user and editing it.
It is important to mention that we also created a src/AppBundle/Services folder with some helper files to manage common functionalities across the controllers, such as the JSON serialization and management and the “JwtAuth,” which creates a real-time token using user info and the time. This token will be required for any action after the user login.
# to generate the token
if($signup){
//generate token
$token = array(
"sub" => $user->getId(),
"email" => $user->getEmail(),
"name" => $user->getName(),
"surname" => $user->getSurname(),
"iat" => time(),
"exp" => time() + (7*24*60*60)
);
$jwt = JWT::encode($token, $this->key, 'HS256');
$decoded = JWT::decode($jwt, $this->key, array('HS256'));
/code
/code
# to validate the token in any action
$helpers = $this->get(Helpers::class);
$jwt_auth = $this->get(JwtAuth::class);
$token = $request->get("authorization",null);
$authCheck = $jwt_auth->checkToken($token)
Now, after this setup and implementation overview, these are the current actions/endpoints that the backend supports:
(My local apache is responding to: 127.0.0.1)
Login – http://127.0.0.1/task_app/symfony/web/app_dev.php/login :: type POST Body:
key = json value = {“email”:”admin@admin.com”,”password”:”admin”} this will generate a token that will be required for all authorization validations.
New user – http://127.0.0.1/task_app/symfony/web/app_dev.php/user/new :: type POST Body:
key = json | value = {“name”:”admin55″,”surname”:”admin55″,”email”:”admin55@admin55.com”,”password”:”admin55″}
Edit user – http://127.0.0.1/task_app/symfony/web/app_dev.php/user/edit :: type POST Body:
key = json | value = {“name”:”admin”,”surname”:”admin”,”email”:”admin@admin.com”, “password”:”admin”} key = authorization | value = eyJ0eXAi… (token returned by login)
Task list – http://127.0.0.1/task_app/symfony/web/app_dev.php/task/list :: type POST Body:
key = authorization | value = eyJ0eXAi… (token returned by login)
New Task – http://127.0.0.1/task_app/symfony/web/app_dev.php/task/new :: type POST Body:
key = json | value = {“title”:”title3″,”description”:”description2″,”status”:”new”} key = authorization | value = eyJ0eXAi… (token returned by login)
Task Detail – http://127.0.0.1/task_app/symfony/web/app_dev.php/task/id/33 :: type POST Body:
key = authorization | value = eyJ0eXAi… (token returned by login)
Search Task – http://127.0.0.1/task_app/symfony/web/app_dev.php/task/search :: type POST Body:
key = filter | value = 1 ( task type id = 1 = New, 2 = TODO, 3 = Finished) key = order | value = 0 (0,1) key = authorization | value = eyJ0eXAi… (token returned by login)
Task Remove – http://127.0.0.1/task_app/symfony/web/app_dev.php/task/remove/3 :: type POST Body:
key = authorization | value = eyJ0eXAi… (token returned by login)
That sums up the basics of how the backend was created; it provides a nice picture of this specific Symfony architecture. Let’s continue with the client side.
Client Side
Angular 4 was selected to manage the client side in this example because of its consistency, productivity, maintainability, modularity, and great ability to catch errors. With lots of documentation, Google’s endorsement, and extensive community support, Angular 4 is one of the strongest client-side MVC frameworks you can work with.
The project was created as one would expect:
ng new project
npm start
ng serve
The app will run in http://localhost:4200/
Inside the package.json, we only added new instances for Jquery and Bootstrap.
This is the basic structure that the app will use:
Please check the assets, CSS, and img folders; they were created to manage the resources. Very important: inside the “src/app/services”, you will see “global.ts”; this file contains the path to the backend server, which in our case is this:
‘http://127.0.0.1/task_app/symfony/web/app_dev.php’
Additionally, the services folder contains the task.service.ts and the user.service.ts , which contain all the functions to consume and send info across the backend endpoints.
For example, if you go to task.service.ts , the “create task” action is like this:
create(token, task){
const json = JSON.stringify(task);
const params = `json=${json}&authorization=${token}`;
const headers = new Headers({'Content-Type':'application/x-www-form-urlencoded'});
return this._http.post(`${this.url}/task/new`,params, {headers:headers})
.map(res=>res.json());
}
This function is used by the onSubmit event in the
src/app/component/task.new.component.ts
And the data is gotten by the form in the view “src/app/view/task.new.html”
. This is the basic communication flow between the view, controller and service. These are important things to have in mind in case the security token is required by the service. The token is obtained as follows:
getToken(){
this.token = JSON.parse(localStorage.getItem('token'));
return this.token;
}
Inside the “src/app/services/user.service.ts”, as you can see, the local storage contains the token that was previously added after the sign up in login.component.ts .
localStorage.setItem('token', JSON.stringify(this.token))
It is important to note that every time a new action is created, the modules and components should be added in:
- src/app/app.module.ts
- src/app/app.routing.ts
This Angular app is actually very basic in terms of comprehension; the MVC is very intuitive, and the view doesn’t contain any animations or something strange. It only uses the column format and classes from Bootstrap to perform some validations such as:
<div class="alert alert-success" *ngIf="status_task === 'success'">
Task Created
</div>
<div class="alert alert-danger" *ngIf="status_task === 'error'">
Task was not created!
</div>
But internally, the field mapping is very straightforward.
We are using a pipe in “src/app/pipes” to transform the DateTime format into something like this: “07/02/2018” .
Take your time to look at the components, views, and services; that’s the idea of this post.
We’ve created a basic architecture using two powerful tools: Symfony and Angular. In the end, the app should look something like this:
I hope that this post provides you with a good introduction to this kind of full-stack architecture. This is just another flavor in the ocean of recipes, but in my opinion, using Symfony as a backend framework is a really good idea. It has a big community, is easy to learn, is very modular, and in a short amount of time, you can use it to get strong web apps up and running. As for Angular, it’s a great tool, and thousands of developers around the world think the same.