This is a book documenting the creation of SongBird, a simple CMS created using Symfony Full Stack which itself consists of many Symfony Components. Although the resources in symfony.com is great, they are often hard to digest and you need to piece all the code snippets up to build something useful. I wanted to write an application that covers different aspects of Symfony as much as possible. I believe this approach will be helpful for people who are new to Symfony. This idea was conceptuaised in early 2015 but the implementation turned out to be much longer that I thought - I had to upgrade Symfony from 2 to 3 and dropped SonataAdmin in favor of EasyAdmin when I was 90% done.
The project was finally completed in Aug 2016 after much persistence and has gone through several revisions since.
The objective of this project is to:
Illustrate the power of rapid development with Symfony.
Reduce the learning curve by sharing step-by-step guide to create a working application. This process should be helpful to anyone who wants to dive into Symfony.
Kickstart simple Symfony projects with this application.
Share the fun of programming.
It is important to note that the rationale behind SongBird CMS is to have broad coverage. Therefore, I try not to repeat the same techniques in every chapter. In reality, you might find some techniques work really well and will want to keep reusing them. On the other hand, you might find certain implementations overkill. This is all for learning purpose.
I hope you have fun creating your own version of SongBird by following the exercises in this book, choosing your best arsenal for your future adventures.
Cheers,
Bernard Peh
Introduction
Building an application is like building a Pyramid. You create each piece of functionality bit by bit. You test the functionality and make sure its stable before building the next piece. This cycle continues until you reach the peak - completion.
Choosing the best framework for RAD (Rapid Application Development) has been a topic debated to death. Today, there is no longer such a thing as “The Best Framework” because all modern day frameworks follow the best practice. However, there is such a thing called “The Best Practice”. In fact, you can see similar development methodologies being used across all frameworks. So knowing one framework well means you can jump between other frameworks easily. Just as human evolves, different frameworks learn from each other and adapt very fast to new and better technologies.
At the time of writing, NodeJS continues to innovate with PHP closing in fast behind. PHP is the old veteran when comes to web development with the most frameworks in the market. The 2 frameworks that stood out from the pack were Laravel and Symfony. If you are looking to learn a new framework, I highly recommend Symfony because it is one of the more stable modern framework out there. Symfony components have been widely used by many popular projects including Composer, Behat, Codeception, Drupal and Laravel (just to name a few).
Learning Symfony is never an easy task. Many people follow tutorials in google, read up all the documentation in Symfony website and still find it challenging to create a simple application. Why? Because there is too much theory and not enough real world practical examples. Worst still, you can get entangled in technical jargons and advance customisations easily. The fact that Symfony is an extremely flexible framework makes it even harder to master because there are so many ways to achieve the same goal. If you are new to MVC (Model-View-Controller) and RAD, you will find that Symfony has a steep learning curve.
This book aims to lower the learning curve by providing a step by step hands-on approach to guide developers who are new to Symfony to build a simple CMS using good industry practice. Let us call the CMS “SongBird”. Hopefully after following all the chapters, your eyes will be opened to RAD and the unlimited possibilities with Symfony.
Audience
This book is targeted at developers who are new to Symfony. If you are already a seasoned PHP Developer, I hope you would pick up some tips here and there.
Why Re-invent the Wheel?
At the time of writing, there are already many CMS and a few popular Symfony ones out there. Symfony has the CMF project. Why built a new one?
SongBird is really a tutorial project and not trying to compete in the CMS space.
Is SongBird Reusable?
Definitely. SongBird is not just a tutorial CMS, you can use it as a vanilla framework to kickstart other Symfony projects. It will be a hugh time saver because all common features have been build and configured already. Since you are the one who creates the software, you will have better idea of how the software works and know where to customise things should the need arises.
You can also think of SongBird as the foundation for the CMF project. Once you are comfortable with the basics of building a CMS, you are ready for more complex development.
Chapters Overview
Chapter 1: Survival Skills
A quick introduction to the skills required to learn Symfony.
Chapter 2: What is SongBird
Introduction to what Songbird is and isn’t.
Chapter 3: Creating the Dev Environment
Installing Songbird using docker. Docker is fantastic but its a shame that mac users need a work around at the time of writing.
Chapter 4: The Testing Framework Part 1 (Optional)
Introduces and Installing Codeception for Behavioural Testing.
Chapter 5: The Testing Framework Part 2 (Optional)
Writing a sample BDD acceptance test.
Chapter 6: The User Management System Part 1
Introduces and Installing FOSUserBundle for User Management.
Chapter 7: The User Management System Part 2
Generating user CRUD using the command line and a bit of doctrine appetizer.
Chapter 8: Doctrine Fixtures and Migrations
Installing and running Doctrine Fixtures and Migrations. It is important to have a consistent way of creating test data and migrating schemas.
Chapter 9: The Admin Panel Part 1
Installing EasyAdminBundle and integrating it with FOSUserBundle.
Chapter 10: BDD With Codeception (Optional)
Writing BDD acceptance tests for user management business rules.
Chapter 11: Customising the Login Process
Customising Twig templates for the login and request password pages.
Chapter 12: The Admin Panel Part 2
Tweaking EasyAdmin UI.
Chapter 13: Internalisation
Getting Songbird to support french as well.
Chapter 14: Uploading Files
Installing VichUploaderBundle and integrating it with EasyAdminBundle.
Chapter 15: Logging User Activities
Creating a simple bundle to log user activities.
Chapter 16: Improving Performance and Troubleshooting
Installing blackfire and improving Symfony performance. Introduces Gulp to manage all frontend assets.
Chapter 17: The Page Manager Part 1
Creating a custom bundle called NestablePageBundle to manage pages. Introduces PHPUnit to write functional tests.
Chapter 18: Making Your Bundle Reusable
Refactoring NestablePageBundle and making it as a separate installable component.
Chapter 19: The Page Manager Part 2
Installing CKEditor to the CMS and creating a custom locale selector.
Chapter 20: The Front View
Creating the frontend controller and view.
Chapter 21: Dependency Injection Revisited
Using Compiler Pass to add user roles to EasyAdminBundle.
Chapter Final
Congratulations. It’s time to start build something yourself using Symfony.
Conventions Used in This Book
Each git branch is a chapter. Obviously, chapter_6 branch means it is Chapter 6. Otherwise stated, all path references assumes ~/songbird as the root folder. Always execute commands from the root folder.
To executing commands, You will see a “->” before the command. For example
This means that in the command line terminal, go to the ~/songbird folder and type in “git status”.
Likewise, a code snippet like this
means update or insert this snippet in ~/songbird/symfony/app/config/routing.yml
or it could mean a comment for you to action like
All symfony commands runs under the symfony dir, ie
Learning Symfony
If you are new to RAD and like to learn Symfony, I recommend you to go through the chapters in sequential order. Every time you are on a new chapter, create a new branch based on the previous chapter and try to add or update the code as suggested in the chapter. For example, you have just finished chapter 4 and going into chapter 5.
Commit all your changes in chapter 4 first.
Then checkout chapter 5.
We use mychapter_x to differentiate between your work and my work. To look at all the branches available:
If you are being lazy and want to use my chapter 4 instead to start chapter 5,
If you are already getting confused, here are some good git resource to read.
Jumping between Chapters
I have organised the repository such that every chapter will have its own corresponding branch in the code. Feel free to jump between the different chapters and test out the code. However, remember to stash or commit your changes before switching to a new branch. Also remember to clear your cache if things are broken.
Chapters that talk about Codeception Testing Framework are optional. Feel free to skip them if you already know testing.
To clear the cache fully,
Regenerating Bootstrap Cache
If you are getting errors on bootstrap.php.cache, you can regenerate it easily.
A common issue is when you get allowed memory exhausted error. A quick workaround is
Reinstalling Symfony
Some directories are needed by Symfony but they are not version controlled (eg. the bin directory). In case they have been deleted accidentally, you can reinstall the packages. The re-installation process will not mess up with your existing code. That’s the beauty of being modular.
Installing the Demo (Optional)
If you are already getting impatient and wants to see a demo of the completed project, you can checkout the final chapter. Make sure you have docker and docker-compose installed.
Docker could be slower for mac users. Chapter 3 provides a workaround.
Now go to http://songbird.app:8000 and you should see the homepage.
You can log into the backend by going to http://songbird.app:8000/admin using
Without a doubt, the 2 biggest Symfony resources on the web at the moment are “The Book” and “The Cookbook”, both can be downloaded from Symfony Documentation Page. Cudos to Fabien and the team behind the books, making Symfony one of the best documented frameworks out there. Having said that, the content in these 2 books are hard to digest and almost impossible to follow unless you have good foundation in Object Oriented Programming. There are a lot to go through. Even if you are have the skills, you will need enough determination to read them. Even if you finish reading them, you still need to have enough practical experience to digest the theory.
I hope there is really a simple formula to become a Symfony ninja overnight…
The Tools You Need
You will need to equip yourself before diving in. Ideally, you have
A good computer. I recommend a modern day Mac not more than 4 years old with at least 100G of free space to setup development environment. Mac is fast becoming the new standard for coding. Linux is fine. If you insist in windows, make sure you have command line - cygwin is a good option.
Good foundation in programming. Experience with Object Oriented Programming and relational databases is recommended.
Understand Dependency Injection (DI). Fabien wrote a good article about DI. DI is the heartbeat of Symfony and most modern day framework.
Good source control knowledge, especially Git and Git Flow.
Basic HTML, CSS and Javascript knowledge.
Basic Stylesheet Pre-processor language like LESS or SASS.
Basic Linux command line knowledge.
A good IDE. There are lots of them out there. Sublime Text is OK but PHP Storm is way better for serious Symfony development.
I hope the list doesn’t scare you to get started.
Using the Command Line
I suggest you to get comfortable with the command line. Many modern day frameworks use command line to automate tasks. In this book, I’ll be using a lot of command line but I suggest you not to memorise them. Always type “app/console” to see the options and then narrow in from there.
For example, your app/console might look like this (doesn’t matter if it doesn’t at this point):
Wow, that is a lot but don’t worry, you will get used to the important ones after finishing the book.
Selling Your Soul to the Demon
Many people use web frameworks to create internet applications nowadays. A framework speeds up web development by giving you automation tools to create commonly used features like user management system, forms, pages, menus…etc. This means that you can create these features easily without knowing how they work. It is like buying a car without knowing how the car works. This is all good until if you want to customise the inner components or repair it. You could get someone to customise the car (hire a developer) or DIY.
If you are a developer, there is value in learning how to built a CMS with Symfony. While building the CMS, you learn how to configure and customise all the bundles to make them work together. As the builder, you will know where to start troubleshooting when things go wrong.
Let’s get the ball rolling…
Summary
This is a short chapter. We discussed the basic skills required to learn a modern day framework like Symfony. You were mentally prepared and warned about the pros and cons of using a framework.
In a nutshell, SongBird is a bare bone CMS (Content Management System) consisting the following features:
Admin Panel and Dashboard - A password protected administration area for administrators and users.
User Management System - For administrators to manage users of the system.
i18n Capability - Multi-lingual. No CMS is complete without this.
Page Management System - For managing the front-end menu, slug and content of the site.
User Logging Sytem - For logging user activities in the backend.
Frontend - The portal where the public interacts with the site. No login required.
We will attempt to built the CMS using some popular modules available to cut down the development time. This is the best approach. However, that also means that we lose the fun of building some cool bundles ourselves. In view of that, we will attempt to build the Page management bundle and frontend ourselves.
So What is the Plan?
In this chapter we are going to define the scope of the software. People spend weeks to write a proper functional specification for a software like this. Functional Specification defines the scope of the project, provides an estimate of the amount of man hours required and duration to complete the job, gives people an idea of what the software is, what it can or can’t do. It is also important to use that as a reference when writing test cases as well.
Writing good functional specs is the most important part of the Software Development Life Cycle. In our case, we shall cut down the words and show only relevant information in developing SongBird.
Use Case Diagram
This is a high level overview of the roles and features of SongBird.
Database Diagram
The entity relationships in a nutshell. In the real world, the relationships won’t be that simple. You should see more one-to-many and many-to-many relationships.
User Journey
This is how I visualise a user would interact with the website. Hopefully, it gives you confidence of what we are about to build.
a) The frontend homepage:
b) The frontend subpage:
c) The login page:
d) Backend dashboard:
d) Backend listing page:
e) Backend record edit page:
We haven’t started coding but you already have a realistic view of the final product.
Sitemap
We are going to start with a few pages only, keeping the navigation simple.
User Stories
A user story defines the functionality that the user wants to have in plain english. We don’t want to drill down to specifics at this stage. The specifics should be in the user scenarios. We make use of “As a”, “I want/don’t want to” and “So that” to help define good user stories.
As an example:
“As a developer, I want to create a simple CMS, so that I can use it as a vanilla CMS for more complex projects”.
We will define the user stories for each chapter as we go along.
User Scenarios
User Scenarios break the user story down into further possible outcomes. I like to think of them as pseudocode. We make use of “Given”, “When” and “Then” to define user scenarios. BDD tests are written based on these scenarios. Based on the example above, Possible scenarios are:
“Given the homepage, When I land on the homepage, Then I should see a big welcome text.”
“Given the about us page, When I navigate to the about us page from the menu, Then I should see my name”
We will define the user scenarios based on the user stories for each chapter as we go along.
Summary
In this chapter, we tried to define what SongBird is and isn’t. We defined the requirements and provided some use cases/diagrams to help define the end product. In real life, requirement docs could be a lot longer. Having well defined requirements is paramount in building robust software.
So that we speak the same language throughout the book, we need a dev (development) environment that it is consistent in everyone’s host. We will use docker for this purpose.
The idea is to do actual coding in your host (main operating system) and let docker other services like the web server, MYSQL … etc. Note that 99% of the time, you don’t need to touch the docker instances except to make sure that they are all up and running.
Add songbird.app to your host file.
# for unix systems
-> sudo echo "127.0.0.1 songbird.app" >> /etc/hosts
We have mapped port 8000 to our nginx web server, open up browser and go to http://songbird.app:8000. If you see an installation successful page, you are on the right track.
Let us configure the dev url to allow connection from the parent host
Now try this url http://songbird.app:8000/app_dev.php and you should see the same successful page but with a little icon/toolbar at the bottom of the page. That’s right, you are now in dev mode. Why the “app_dev.php”? That is like the default page for the dev environment, something unique to Symfony which we will always be using during development.
To check that everything is working, let us look at the logs
Good, nginx and symfony is logging stuff.
Every time your machine restarts, remember to start docker, then run docker-compose up -d in the songbird folder to start the dev environment.
Finally, let us ignore .env in .gitignore
Mac Users (Optional)
Docker is such an amazing tool and I think it will only get more popular. However at the time of writing, mac operating system suffer performance issues due to osxfs. We can improve the disk access speed by using nfs instead. You can google about this and read about the technical details.
To mount via nfs, click on the docker icon on the top of your desktop -> Preferences -> File Sharing (remove all mounted dirs except /tmp) -> Restart docker.
We can then export the whole /Users dir
Summary
In this chapter, we setup the development environment using docker. We have installed Symfony and configured the host to access SongBird from the host machine.
Remember to commit all your changes before moving on.
Exercises (Optional)
Try running Symfony’s build-in webserver. What command would you use? What are the pros and cons of using the build-in webserver?
Delete the symfony dir. Reinstall Symfony following the Symfony Installation instructions.
How many ways are there to install Symfony? What are the pros and cons of each?
Chapter 4: The Testing Framework Part 1 (Optional)
This chapter talks about Codeception. Feel free to skip it if you already have a testing framework in place.
No application is complete without going through a rigorous testing process. Software Testing is a big topic by itself.
Today, many developers know TDD and BDD. Test First Development ensures that your software is reliable but requires a lot of patience and extra work to implement it correctly. Think of it like a quality control process. The more checks you have, the less bugs your have. Of course, you can cost cut by not having checks and hope that your product is still bug free. This is quite unlikely especially if the software is complex.
Personally, I prefer to write user stories and scenarios first rather than spending time coding the tests. Think of them as pseudocode. Once we have the user stories and scenarios defined, we will jump in and code functionality A. When functionality A is completed, we will code the test cases and ensure they pass before moving on. We will repeat the cycle for functionality B before moving on to functionality C. The idea is to not break existing functionalities while adding on new functionalities.
Everyone’s testing approach is different. You could implement your own approach.
There are many frameworks for acceptance testing. Behat and Mink are the industrial standard at the moment. In this book, we will be using Codeception to write acceptance tests in most cases. We will also be writing some functional test in phpunit.
Installation
The “–dev” means we only need this in dev mode. If everything is working, you will see composer adding the dependency in composer.json
Now we can initialise codeception
Let us configure the acceptance test.
Acceptance Testing is like Black Box Testing - We try to simulate real users interacting with our app. We ignore the inner workings of the code and only care if it works from the end user’s point of view.
Here, we are using the headless browser - phantomjs to connect to the webserver at 172.25.0.5 (see the docker-compose.yml file). Codeception by default comes with PhpBrowser which doesn’t support javascript. Selenium is slow but is the veteran when comes to acceptance testing. Feel free to switch to selenium if you encounter problems.
We can now generate the acceptance actions based on the updated acceptance suite:
The First Test
We know that the default Symfony comes with the AppBundle example. Let us now test the bundle by creating a test suite for it.
The auto generated Cest class should look like this:
Let us write our own test. All new Symfony installation homepage should have a successful message.
We have been running the codecept command from the host machine. That is fine but we should really be running the command in the php docker container. In the symfony dir, we need to softlink the .env file as we are going to run docker commands in that dir. If not, you will get a bunch of environment variables not found error.
Now run the test:
Some files such as images are binary. We need to tell git not to convert the line endings (google for it if interested)
Don’t forget to commit your code before moving on to the next chapter.
Summary
In this chapter, we discussed the importance of testing and touched on TDD and BDD. In our context, we will be mainly writing BDD tests. We installed codeception and wrote a simple acceptance test to tests the default symfony home page.
Exercises (Optional)
Try configure codeception to allow the running of different acceptance testing profiles. Can you test with PhpBrowser or selenium easily? Do you see any benefit of doing that? See advanced codeception for help.
Chapter 5: The Testing Framework Part 2 (Optional)
This chapter talks about Codeception. Feel free to skip it if you already have a testing framework in place.
Since we are ready to build the application, let us remove the route for the default homepage and we are going to make sure that we have done that correctly.
Modifying DefaultController.php
Previously, we could access the route “/” because the route exists in DefaultController.php. Removing the @route annotation will remove the route. A simple trick to do that is to take out the @.
Now, refresh http://songbird.app:8000/app_dev.php and you should see a 404 error.
This is correct because the url is no longer configured. How can you be sure? Let us check it out from the command line
Looks like there is no trace of the / path. This is all good but to do a proper job, we have to make sure that this logic is remembered in the future. We need to record this logic in a test.
Hang on, did you realise how ugly the command line is?
docker-compose exec php bin/console debug:router
What we are doing here is that we are trying to access bin/console within the php docker instance. If you have php 7 installed in your host, you could run bin/console straight away and achieve the same results.
We will create a much simple wrapper around the docker commands in a minute.
Making sure the / route is removed
To make sure that / route is correctly removed and not accidentally added again in the future, let us add a test for it.
and run the test again,
Creating custom bash script to run acceptance test
We have to remember to clear the cache every time we run the test so that we don’t test on the cached version. Let us automate this by creating a script in the scripts dir called “runtest” and make it executable.
In the runtest script,
Now test your automation by running
We are almost done. Remember to commit all your changes before moving on to the next chapter.
Summary
In this chapter, we have removed the default / route and updated our test criteria. We have also created a few bash scripts to automate the task of running codecept test. We will add more to these scripts in the future.
User Management System is the core part of any CMS. We will create this feature using the popular FOSUserBundle.
Pre-setup
Make sure we are in the right branch. Let us branch off from the previous chapter.
Installing the FOSUserBundle
Add the bundle in composer.json
Now in AppKernel, we need to register the bundles
AppBundleUser() will look for the User class in User.php (under the AppBundle namespace). We want the User class to inherit all properties of FOSUserBundle. Let us create User.php
Next we need to configure FOSUserBundle. Don’t worry if certain directives don’t make sense. It will as you progress further. Note that yaml files cannot contain tabs.
and setup the security and firewall, your file should look like this
DB credentials
The db credentials are in app/config/parameters.yml. They are usually variables based on your environment. Since we are using docker, we can hard code them.
Have a look at the file if you are interested.
Creating the User Entity
Have a quick read if you are unfamiliar with doctrine and entity. We will be using doctrine very often in this book.
Symfony allows us to automate lots of things using command line, including the creation of entities. We will create the user entity with 2 custom fields called firstname and lastname.
You realised we have to use “docker-compose exec php” to run commands in the php container. Its ugly and we will automate that in a minute. Once you are familiar with the command line, you should be able to generate the entity and other files without prompts. We will be doing that in the future chapters.
FOSUserBundle Groups are useful when you want to group users together. For the sake of simplicity, we won’t be using this feature. However, you should be able to add this feature in easily once you are comfortable with the Symfony workflow.
Now, the entity class is generated under src/AppBundle/Entity folder. We need to extend the fosuserbundle and make the id protected because of inheritance. If you open up the file, you will see that the code has been created for you already but we still need to make some changes in order for the entity inheritance to work. Refer to comments in the code.
You will noticed all the getters and setters have already been generated for you as well. Cool!
Now, we need to configure the routes. The default routes provided by FOSUser is a good start.
To check that the new routes have been installed correctly,
or we can use the router:match command to match the exact url and get more details
See how much work has done for you by inheriting the FOSUserBundle… This step allows you to use many default FOSUserBundle functionalities like password reset and user profile update without writing a single line of code! Now, let us test one of the routes by going to
You should see a simple login page.
To verify that the schema is correct, let us generate it:
Let us check that the schema has indeed been created correctly.
Looks like we got the right fields. Let us now create a console wrapper to make our life easier.
Wrapper Scripts
We now need a very simple wrapper to run the console commands. Let us create a console wrapper.
once the console script is created, it needs to be executable.
Let us try some commands
Let us do the same for the composer command
and
Finally, we will create another for the mysql command
now we allow executable bit to this script.
We can now use some wrapper scripts to access the php container easily. We are gearing up. Ready for more?
Summary
In this chapter, we have installed FOSUserBundle and extended it in AppBundle. We have verified that the installation was correct by looking at the default login page and database schema. We also created some helper scripts to help accessing the docker instance a bit easier.
Remember to commit all your changes before moving on.
Exercises (Optional)
Try installing the UserBundle outside of Appbundle. Are there any pros and cons of doing that as compared to putting all the bundles in AppBundle?
We have installed the FOSUserBundle but it looks like there are still big chunks of functionalities missing. How do we (C)reate, (R)ead, (U)pdate and (D)elete a user or group for example?
You see the word “CRUD” appearing so many times because it is part of RAD. All frameworks today come with auto CRUD generation.
Automated User CRUD Generation
We will generate CRUD for the UserBundle.
Now go to
We haven’t added any data yet. The database should be empty as per the previous chapter.
Let us add some data. Click on “Create a new entry” or go to
and enter a dummy firstname and lastname, then click create.
You should see a “Integrity constraint violation: 1048 Column ‘username’ cannot be null” error. Why?
I am going to skip through all technicalities for now and tell you where the answer is. Look at
It is possible to create a new user from command line, the code is at:
Did you remember that the “fos:user:create” command is available under the scripts/console command? You can infer from these lines that username, email and password are compulsory. How do we add these extra fields in the user form?
Adding Fields to the User Form
The extra FOSUserBundle fields were not automatically added when we created the CRUD using the command line. The automated CRUD creation process cannot pick up inheritance yet (I hope one day it will). We have to create the fields manually.
Refresh the browser and if changes are not showing up, we need to delete the cache.
This cache:clear command is equivalent to “rm -rf var/cache/dev”. It is a useful alternative to clear:cache. If no environment is set, the environment is set to develop. To delete prod cache,
Let us create 2 test users, say “test” and “test1”
We can now list them by going to /user
Now verify that the new data is inserted into the user table by running some sql
Wow, why was the password exposed? shouldn’t the password be encrypted automatically?
No, because the CRUD that we have created previously didn’t know that the password was supposed to be encrypted before inserting into the db. Fortunately, FOSUserBundle has a service container that can help us with this. The word service is important in Symfony. Don’t worry about it for now as we will cover this in the following chapters.
For the sake of curiousity, let us see all the FOSUserBundle service containers.
The logic for all user related actions is stored in FOSUserBundleDoctrineUserManager. The service for that class is fos_user.user_manager. Let us use the service in UserController.php
The persist and flush statement in doctrine is a standard way to prepare and save queries to db. We have commented it off because if you look at the updateUser function in FOSUserBundleDoctrineUserManager, this part was already done.
Let us try creating a new user called “test3” and view it again in mysql
The test3 user password is now encrypted. Update the password of another user and you will see that the encryption is working.
What’s Up With Editing the User
Now, let’s try editing the test user. We are going to change the first name for example,
The form is stopping us from editing because the password is a compulsory field. How do we fix that?
Let us pass a passwordRequired variable into the UserType class. If the variable is false, the password field will not be compulsory.
and in UserType.php,
If the password field is null, it means that user doesn’t want to update the password. We will need to override FOSUserBundle setPassword function.
Updating Doctrine Fields Automatically
We like to have 2 more fields. We like to know when the user is being created and updated. How do we do that? HasLifeCycleCallBacks annotation is the magic.
The “@ORMHasLifecycleCallbacks()” tells doctrine to run callback functions (in this case, prePersist or preUpdate) before creating or updating an entry.
Let us auto-generate the setters and getters for the new $modified and $created variables.
The –no-backup option tells the command not to back up your original entity file.
Verify that the new getters and setters for $created and $modified have been added to src/AppBundle/Entity/User.php. The schema is now changed and we need to update it.
Try adding a new user and see if the created and modified time have been updated.
Deleting Users
No problem. This should work out of the box. Test it out in your browser to convince yourself.
Cleaning Up
let us clean up the Controller by deleting the DefaultController.php
and we need to update our runtest script
Run a quick test again and make sure that whatever you have done doesn’t break anything. Still remember how to do it?
You will soon realised you need a consistent set of test data to make testing easier. That is why data fixtures are so important.
Summary
We have created User CRUD using command line, digged into the code and fixed up a few things. Even though things still doesn’t work out of the box, we owed a lot to RAD to help us create a user management system in a short time. In reality, most CMS should allow you to configure user management system out of the box. It is still a good practice for us to go through it.
In addition to the basic CRUD, we have added 4 extra fields (firstname, lastname, created, modified). Unlike username, email and password fields, the firstname and lastname fields are not compulsory. On the edit page, the password field is also not compulsory.
Remember to commit all your changes before moving on.
Exercises (Optional)
FOSUserBundle provides a functionality to manage users via command line. Try adding a user from the command line.
Looking at AppBundleFormUserType, what happens if you change the password field to be called “plainPassword” instead? What changes would you make to the UserController.php class if that is the case?
Can you think of another way to pass variable from the controller to the form?
As of now, we could create and manage users via the command line (scripts/console fos:user:xxx) or using the basic CRUD UI that we have created. What if we messed up the data or if we want to reset the data with certain values confidently? How can we do that efficiently? We need an automation mechanism to create consistent schema and dummy data.
Install DoctrineFixturesBundle
Install via composer
Now that the data-fixtures-bundle is installed, we can update the kernel.
We register the bundle under the array(‘dev’, ‘test’) environment because we don’t need this bundle in the production environment.
To prove that the install is successful, we should have a new entry in the console
Create User Fixtures
Let’s create the the data fixtures directory structure
Now create the class. We are going to create 3 users. One super admin, 3 test users, ie test1, test2 and test3.
The username and password for the 3 users are as follows:
Remember these 3 users credentials as we will be using them a lot throughout the whole book.
Now the actual fixtures class:
Now, let us insert the fixtures by running the command line
The “-n” option simply answer yes when prompted for data purging. Try it without the “-n” option for yourself. Verify that the data is inserted by running a simple query
The nice thing about creating fixtures is that you learn a lot about the Entity when you insert the data. Take the FOSUserBundle for example, you need to know about the userManager in order to create encrypted passwords correctly. This knowledge is valuable when writing test cases.
This line shows the power of a modern day framework:
We are trying to use the userManager class using the fos_user.user_manager service. Where is this class?
So basically, we are instantiating FOSUserBundleDoctrineUserManager without including the class and we do it as and when we want it. This is called Lazy Loading. Traditionally, we would require the class and use the “new” keyword, something like this:
Remember we talked about services in the previous chapter? We will see a lot more of these in the later chapters.
Doctrine Migrations
Doctrine migrations allow us to migrate db changes easily. This is important especially when we want to make changes to production db. For example, if production db is a few versions behind, do we upgrade the db sequentially and safely?
Let us start the installation:
and update AppKernel
We also need to configure it.
If the installation is successful, you should see some new migrations commands added:
and
Its the first time we are using it, so we need to generate the initial migration class
Look at “Version20170128004532.php” and you won’t see much in there.
Create Script to Reset Schema and Fixtures
Every time we want to work cleanly, we want to be able to run a script to reset the database and insert dummy records. Let us create a script called resetapp that resides in scripts dir.
Make sure the script is executable
Now we can run the test
Update runtest script
The runtest script can now call the scripts/resetapp script to have a cleaner start before running the test
What is “$@”? In bash, it means putting in the command line options that was passed into the runtest script. We can now execute only the RemovalTest like so:
Summary
In this chapter we learned how to install the doctrine fixtures and migrations bundle. We also created a fixture class for our user bundle. We then upgraded our runtest script to reset the db and load the fixtures before running the test.
Remember to commit all your changes before moving on.
We have used FOSUserBundle to create a User CRUD in the previous chapters. It’s looking ugly at the moment but its functional. However, anyone can access the user management if they have the right url. We need an admin area where administrators can login and manage the users. All administrative activities should happen behind the admin url, something along the lines of /admin/users for example.
Again, we will try to simplify the process by reusing a 3rd party module that others have created. SonataAdmin and EasyAdmin are quite popular at the moment. SonataAdmin is more advanced but more complex to setup. In this book, we will be using EasyAdmin to build the admin panel.
It wouldn’t be fun if we just use the ready made solution. In this and the next few chapters, we will attempt to build up the admin area bit by bit.
Install EasyAdminBundle
As usual, let us add the required bundles in the composer.json file
and remember to activate the required bundles in AppKernel.php
Create a new easyadmin config file
The main config file then needs to load everything under the easyadmin folder
and routing file
If everything goes well, there will be new routes added
We will install the default styles from the bundle
Say for now, we want ROLE_USER to access the admin dashboard.
Let us create the new admin controller
Now, try logging in
By default, the admin page requires ROLE_ADMIN and above (see app/config/security.yml). So let us login as the administrator
wow, we can now see the admin dashboard. If you have accidentally deleted or modified the admin user, remember that you can reset the db with scripts/resetapp.
Looks pretty empty huh?
Services
services.yml is important because that is where we define reusable components. Let us create a dummy one for now.
Next, we need to create the a yml service extension and the configuration class so that the framework can load it during the bootstrap.
and AppExtensions.php contains
now the configuration file
Filtering the User fields
The user table has many fields. Remember that you specified what fields you want to display in src/AppBundle/Form/UserType.php? By using EasyAdmin, the creation of forms is now managed by the config. It should be self explanatory. Let us modify the fields.
Thanks to easyadmin, we have just created CRUD with this yaml file. We have trimmed down all the fields to include only the relevant ones. Note the plainPassword field - We have created 2 password fields with just a simple configuration.
Navigate the site and make sure they are looking good. Looking at mysql, you can see that the password has also been encrypted correctly, indicating that the AdminController’s preUpdate function is working.
Redirecting Users to Dashboard After Login
Easy.
User Roles and Security
What if we want ROLE_USER to login to /admin but restrict them to certain areas only?
We need to subscribe to some events so that we can add some rules based on user’s role. Remember the services.yml? It will save the day.
Let’s now create the AppSubscriber class
Basically, we have created a checkUserRights function to ensure that other than the super admin, only the rightful owner can edit and see his own profile only.
Try logging in as test1:test1 (user id = 2) and see own profile
If test1 tries to see other people’s profile, we should get an access denied error.
User list url should give us access denied as well.
There is one more thing we need to clean up. If I login as ROLE_USER, I should not be able to see certain fields.
Under the “edit” action, I should not see the roles, enabled, locked, and expired fields.
Under the “show” action, I should not see the created field. User should also be redirected to the show page when the details are updated.
Easyadmin allows creation of isolated entity functions like “editUserAction”. This is brilliant because updating this function won’t affect other entities.
Cleaning up
Since we are not going to use FOSUserBundle /profile url to change update user profile, let us remove it from the routing.yml
Now let us do some cleaning up. Since we are now using EasyAdmin, a lot of files that we have generated using the command line are no longer needed. As you can see, automation is only good if you know what you are doing.
Summary
We have installed a popular Admin system called EasyAdminBundle. We then integrated FOSUserBundle with EasyAdminBundle and customised some fields. We have also configured the security of the system such that unless the logged in user is a super admin, the user can only see or update his own profile.
Remember to commit your changes before moving on to the next chapter.
Exercises
Try installing the SonataAdminBundle yourself and see the differences in both approach.
This chapter is optional, feel free to skip it if you already have your own testing framework in place.
Behavioural-Driven-Development (BDD) is best used as integration testing. It is the concept of writing tests based on user’s behaviour. The way users interact with the software defines the requirements for the software. Once we know the requirements, we are able to write tests and simulate user’s interaction with the software.
In BDD, each user’s requirement (user story) can be created using the following template:
We can then further breakdown the story into scenarios. For each scenario, we define the “When” (user’s action) and the “Then” (acceptance criteria).
It is a good idea to create a matrix for user stories and test scenarios to fully capture user’s requirement as part of the functional specifications.
User Stories
Let us define the user stories for this chapter. We will define the user stories before each chapter from now on.
User Story 10: User Management
Story Id
As a
I
So that I
10.1
test1 user
want to login
can access admin functions
10.2
admin user
want to login
can access admin functions
10.3
test3 user
don’t want to login
can prove that this account is disabled
10.4
test1 user
want to manage my own profile
can update it any time
10.5
test1 user
dont’t want to manage other profiles
don’t breach security
10.6
admin user
want to manage all users
can control user access of the system
User Scenarios
We will break the individual story down with user scenarios.
Story ID 10.1: As a test1 user, I want to login, so that I can access admin functions.
Scenario Id
Given
When
Then
10.1.1
Wrong login credentials
I login with the wrong credentials
I should see an error message
10.1.2
See my dashboard content
I login correctly
I should see Access Denied
10.1.3
Logout successfully
I go to the logout url
I should be redirected to the home page
10.1.4
Access admin url without logging in
go to admin url without logging in
I should be redirected to the login page
Story ID 10.2: As a admin user, I want to login, so that I can access admin functions.
Scenario Id
Given
When
Then
10.2.1
Wrong login credentials
I login with the wrong credentials
I should see an error message
10.2.2
See my dashboard content
I login correctly
I should see the text User Management
10.2.3
Logout successfully
go to the logout url
I should be redirected to the home page
10.2.4
Access admin url without logging in
go to admin url without logging in
I should be redirected to the login page
Story ID 10.3: As a test3 user, I don’t want to login successfully, so that I can prove that this account is disabled.
Scenario Id
Given
When
Then
10.3.1
Account disabled
I login with the right credentials
I should see an “account disabled” message
Story ID 10.4: As a test1 user, I want to manage my profile, so that I can update it any time.
Scenario Id
Given**
When
Then**
10.4.1
Show my profile
I go to “/admin/?action=show&entity=User&id=2”
I should see test1@songbird.app
10.4.2
Hid uneditable fields
I go to “/admin/?action=edit&entity=User&id=2”
I should not see enabled and roles fields
10.4.3
Update Firstname Only
I go to “/admin/?action=edit&entity=User&id=2” And update firstname only And Submit
I should see content updated
10.4.4
Update Password Only
I go to “/admin/?action=edit&entity=User&id=2” And update password And Submit And Logout And Login Again
I should see content updated And be able to login with the new password
Story ID 10.5: As a test1 user, I don’t want to manage other profiles, so that I don’t breach security.
Scenario Id
Given
When
Then
10.5.1
List all profiles
I go to “/admin/?action=list&entity=User” url
I should get an “access denied” error.
10.5.2
Show test2 profile
I go to “/admin/?action=show&entity=User&id=3”
I should get an “access denied” error.
10.5.3
Edit test2 user profile
I go to “/admin/?action=edit&entity=User&id=3”
I should get an “access denied” error
10.5.4
See admin dashboard content
I login correctly
I should not see User Management Text
Story ID 10.6: As an admin user, I want to manage all users, so that I can control user access of the system.
Scenario Id
Given
When
**Then
10.6.1
List all profiles
I go to “/admin/?action=list&entity=User” url
I should see a list of all users in a table
10.6.2
Show test3 user
I go to “/admin/?action=show&entity=User&id=4” url
I should see test3 user details
10.6.3
Edit test3 user
I go to “/admin/?action=edit&entity=User&id=4” url And update lastname
I should see test3 lastname updated on the “List all users” page
10.6.4
Create and Delete new user
I go to “/admin/?action=new&entity=User” And fill in the required fields And Submit And Delete the new user
I should see the new user created and deleted again in the listing page.
Creating the Cest Class
Since we have already deleted the test directory, let us create the testing framework under src/AppBundle
and update the acceptance file again
Codeception is really flexible in the way we create the test scenarios. Take User Story 1 for example, we can break the user story down into directories and scenarios into cest class. Let us create the files:
We will create a common class in the bootstrap and define all the constants we need for the test.
This is the xpath for the show button. How do we know where it is located? We can inspect the elements with the developer tool (available in many browser).
You also noticed that the login class is protected rather than public. Protected class won’t be executed when we run the “runtest” command but we can use it as a pre-requisite when testing listAppProfiles scenario for example, ie the @before login annotation.
listAllProfiles function goes to the user listing page and checks for 4 rows in the table. How do I know about the amOnPage and canSeeNumberOfElements functions? Remembered you ran the command “/bin/codecept build” before? This command generates the AcceptanceTester class to be used in the Cest class. All the functions of the AcceptanceTester class can be found in the “src/AppBundle/Tests/_support/_generated/AcceptanceTesterActions.php” class.
In the test, I used the user listing url directly rather than clicking on the “User Management” link. We should be simulating user clicking on the “User Management” link instead. We will update the test again once we work on the updated UI.
Let us update the runtest script
and update the gitignore path
Then, run the test only for scenario 10.6.1
Looking good, what if the test fails and you want to look at the logs? The log files are all in the “src/AppBundle/tests/_output/” directory.
Let us write another test for scenario 10.6.2. We will simulate clicking on test3 show button and check the page is loading fine.
run the test now
and you should get a success message.
We will now write the test for scenario 10.6.3
Run the test now to make sure everything is ok before moving on.
and scenario 10.6.4
createNewUser test is a bit longer. I hope the comments are self explainatory.
Let’s run the test just for this scenario.
Feeling confident? We can run all the test together.
Want more detail output? Try this
How about with debug mode
Tip: If you are using mac and got “too many open files” error, you need to change the ulimit to something bigger
Add this to your ~/.bash_profile if you want to change the limit everytime you open up a shell.
If your machine is slow, sometimes it might take too long before certain text or element is being detected. In that case, use the “waitForxxx” function before the assert statement, like so
We have only written the BDD tests for user story 10.6. Are you ready to write acceptance tests for the other user stories?
Writing tests can be a boring process but essential if you want your software to be robust. A tip to note is that every scenario must have a closure so that it is self-contained. The idea is that you can run a test scenario by itself without affecting the rest of the scenarios. For example, if you change a password in a scenario, you have to remember to change it back so that you can run the next test without worrying that the password being changed. Alternatively, you could reset the db after every test but this could make running all the tests longer. There are also other ways to achieve this. How could you do it so that it doesn’t affect performance?
The workflow in this book is just one of many ways to write BDD tests. It is worth knowing that at the time of writing, many people uses behat.
Summary
In this chapter, we wrote our own CEST classes based on different user stories and scenarios. We are now more confident that we have a way to test Songbird’s user management functionality as we add more functionalities in the future.
Remember to commit your changes before moving on the next chapter.
Exercises
Write acceptance test for User Stories 10.1, 10.2, 10.3, 10.4 and 10.5 and make sure all test passes.
(Optional) Can you think of other business rules for user management? Try adding your own CEST.
In the previous chapters, we have created the admin area and wrote tests for managing users in the admin area. The login page and admin area is still looking plain at the moment. There are still lots to do but let us take a break from the backend logic and look at frontend templating. Symfony is using twig as the default templating engine. If you new to twig, have a look at twig website. In this chapter, we will touch up the login interface.
Defining User Stories and Scenarios
11. Reset Password
Story Id
As a
I
So that I
11.1
test1 user
want to reset my password without logging in
have a way to access my account in case I forget or loses my password.
Story ID 11.1: As a test1 user, I want to be able to reset my password without logging in, so that I have a way to access my account in case I forget or loses my password.
Scenario Id
Given
When
Then
11.1.1
Reset Password Successfully
I click on forget password in the login page and go through the whole resetting process
I should be redirected to the dashboard.
Customise the Login Page
I have installed twitter bootstrap in the public dir and created a simple logo for Songbird. You can get all the files by checking out from chapter_11 repo. My Resources dir looks like this:
Let us create our own base layout. The idea is to extend this layout for all twig files that we create in the future.
and base.html.twig
To override FOSUserBundle template, have a look at the vendors/friendsofsymfony/Resources/views. Thanks to inheritance, we can override login.html.twig based on the layout that we have created.
Let us create the login file
Now the actual login file:
Once we are done, we can get the assets over to the web dir.
This command basically soft linked everything under the public dir of all bundles over to the document root dir (/web).
Let us go to http://songbird.app:8000/app_dev.php/login and look at our login page now
Installing Mailcatcher
Mailcatcher is excellent for debugging and testing email related functionality. An excellent use case is to test the “forget password” feature.
When using docker, installing a new service is easy (don’t need to worry about dependencies). We just need to get a pre-made mailcatcher image and off we go.
Now let us fire up the new container
We also need to make sure swiftmailer is configured to talk to the new mailcatcher host
Customise the Request Password Page
A full login process should also include the password reset functionality in case the user forgets his password. Fortunately again, FOSUSerBundle has all these features built-in already. We just need to make minor tweaks to the process and customise the templates.
The password reset process is as follows:
User goes to the forget password page.
User enters the username or email.
User gets an email a reset link.
User clicks on the email and goes to a password reset page.
User enters the new password and click submit.
User automatically gets redirected to the dashboard.
We will put a link on the login page to the request password page. We can find all the links from the debug:router command (a command you should be familiar by now)
Let us add the new request password link
By looking at vendors/friendsofsymfony/Resources/views, we can create all the required twig files to override.
Let us create the request password page based on the base.twig.html that we have created earlier.
From the login page, click on the forget password link and you should go to the request password page
Likewise we are going to customise the password request success message.
A successful password request looks like this:
What if you request password reset more than once in a day? FOSUserBundle actually doesn’t allow you to do that.
A screenshot of the password already requested error:
When the password request email is send successfully, the user should request a link to reset the password. Our mailcatcher is configured to capture all emails fired.
Let us go to
If you click on the link, you will go to the actual reset page to enter the new password.
Try entering a new password and see what happens. Nothing? Because we haven’t add the reset.html.twig. Let us do it now.
After you entering the new password and clicking submit, FOSUserBundle will try to redirect you to the fos_user_profile_show route (the profile page which we deleted earlier in route.yml). Since the route no longer exists, you will get an error saying the route no longer exists.
To see what is going on, have a look at vendors/friendsofsymfony/user-bundle/Controller/ResettingController.php::resetAction function. The redirection magic happens after successful form submission.
Let’s say we want to change the redirection to user’s dashboard after successful form submission. What can we do?
Customise the Reset Password Process
We noted that the system dispatches a FOSUserEvents::RESETTING_RESET_SUCCESS event after the form submission is successful. This give us the opportunity to change the response so that the whole redirection logic could be skipped.
Let us update the subscriber class to do our own redirection.
```
# src/AppBundle/EventListener/AppSubscriber.php
…
use FOSUserBundleFOSUserEvents;
use FOSUserBundleEventFormEvent;
use SymfonyComponentHttpFoundationRedirectResponse;
…
# in symfony dir
-> vendor/bin/codecept generate:cest acceptance As_Test1_User/IWantToResetPasswordWithoutLoggingIn -c src/AppBundle
```
To automate the checking of emails, we need the mailcatcher module for codeception. Let us update composer
Let us now update the acceptance.suite.yml to use the new mailcatcher library
we can now rebuild the libraries
# this command will create the mail functions for us to use
-> vendor/bin/codecept build -c src/AppBundle
Do a git diff to see all the mail functions added if you want.
We have updated the aesthetics of the Login and request password change pages. By listening to the reset password event, we redirected user to the dashboard when the event is triggered. Finally, we wrote BDD tests to make sure this functionality is repeatable in the future.
Exercises
(Optional) Try to be fancy with the login layout and css. How do you use FOSUserBundle’s layout.html.twig?
Let us continue with tweaking EasyAdmin by changing the layout and try more adventurous stuff like creating our own dashboard.
Tweaking the UI
Its easy to change the theme colour and add our own custom css. For the sake of manageability, let us create a new file, design.yml
and create a new css
Next, We will will overwrite layout.html.twig by copying it into our own views dir.
We will change the logo and top menu. The top menu will include a link to edit the user and logout.
Let us create the dashboard content.
Noticed how I extended the layout.html.twig and just change the relevant blocks?
Your Dashboard
Let us now create a new dashboard page via the standard way. We need a new route.
now copy the assets to the web dir from command line.
login and then refresh the browser.
Menu Tweaking
Normal users should not see the left entities menus. Again, let us copy the menu.html.twig modify it.
and the actual menu.html.twig
This way of filtering menu access is rather stupid and serves just as an exercise for now. We will describe a better way to user manage our admin area in the later chapters.
Removing hardcoding of admin prefix
There is one more thing to mention before we end this chapter. At the moment, the admin url seems to be prefixed to ‘/admin/xx’. What if we want it to be a bit harder to guess, like ‘admin9/xx’? This is a good security feature. Let us create a variable in the config.yml
We can now use this variable in other places. Once we change this variable, it will automatically update the prefix in all places for us.
and
The default admin page is now default_target_path: /%admin_path%/dashboard
Try changing admin_path to something else and check if all the routes have been updated. Let’s change the admin_path back to ‘admin’ after back.
Update BDD Test (Optional)
Now that we have defined the admin layout, we should update BDD tests for seeMyDashboardContent to test on the dashboard content.
Scenario Id
Given
When
Then
10.1.2
See my dashboard content
I login correctly
I should not see the text “User Management” and should see the text “Dear test1”
Scenario Id
Given
When
Then
10.2.2
See my dashboard content
I login correctly
I should see the text “User Management” and “Dear Admin”
Also with the left menu installed, we should be clicking on the links rather than going to the page directly. In all the test files, replace all amOnPage methods to “click” method.
Once you are confident that all your tests are correct, run it and fix it till everything passes.
Summary
In this chapter, we have touched up the admin area and created a simple dashboard block.
The admin area is now looking more polished.
Exercises
Try creating another url and view yourself. (Optional)
Review and Update BDD for all admin and test1 user stories. (Optional)
No CMS is complete with being being able to support multiple languages (i18n). So far we have been typing english directly into the twig templates. This is quick and easy but not the best practice. What if we are marketing our software to the french market? Wouldn’t it be nice if the interface could be in french rather than english? Its time consuming to create translations for every term that we use but its worth the effort if you want to make your software global.
What about Google Translate? This should be the last option and not be used for professional purposes. Internalisation is something you want to work on early in the software development phase rather than later.
Define User Story
13. Internalisation
Story Id
As a
I
So that I
13.1
test1 user
want to be able to switch language
can choose my preferred language anytime.
Story ID 13.1: As a test1 user, I want to be able to switch language, so that I can choose my preferred language anytime.
Scenario Id
Given
When
Then
13.1.1
Locale in french
I login and switch language to french
I should be able to see the dashboard in french till I switched back to english
Translations for the AppBundle
let us create the translation files in the AppBundle. The naming convention for the file is domain.language_prefix.file_format, eg app.en.xlf.
Let us create the translation directory.
and the actual app.en.xlf
Likewise, we need to create the translation file for french.
Update the Dashboard
How do we get the twig files to do the translation? You would have seen glimpse of it while working with the login files.
Let us update the dashboard template.
refresh your browser and have a look. If things are not working, remember to clear the cache.
By default, we are using english, so you should see that the english version is translated. To see all the translations in english for the AppBundle,
You should see a lot of missing translations for the FOSUserBundle. Don’t worry about that for now.
Tip: Again, don’t remember this command. Just type in “scripts/console debug:translation” in the command line to see the options.
What about french? How do we set the locale? Just update the parameters in the config.yml
Now refresh the dashboard and you should see the welcome block translated.
Its french. Viola!
How do we make the language dynamic? Perhaps we should have a selector on the top menu for users to select the language and persists it throughout the session.
Let us update the translation files
and
and
Now let us update the menu translation in menu.html.twig
Sticky Locale
Let us create the supported languages in config.yml
We have created a variable called supported_lang (consisting of an array) and passed it to twig as a global variable.
Now in the layout twig
Note that we have made logic and css tweaks to the top nav. The new CSS is as follows:
The new language dropdown box allows user to select a language and if there is a change in the selection, the user is redirected to a url /{_locale}/locale where the change of locale magic is supposed to happen.
and create a new controller from the command line.
Tip: This command can save you some time but not much in this case. You don’t have to memorise it. Always use the “help” option if unsure, ie
The controller code in full:
As you can see, the annotation defines the the new route /{_locale}/locale. To make sure that this route is working,
The AdminController gets the request object and redirects the user to the referer if there is one. If not, it redirects the user to either the admin dashboard or the homepage depending if the user is logged in or not. Again, don’t memorise security.authorization_checker. Google around, make intelligent guesses and use the command line to verify the containers.
We said that the controller is the place where magic happens… but where is the magic? We haven’t even change the locale session yet! We cannot change it at the controller level because it is too late. We have to change it very early on in the Http workflow
Basically, what we need to do is to hook on to the kernel.request event and modify some logic there. The symfony cookbook has good information on sticky sessions.
We have create an event subscriber before. As a practice, let us create an event listener this time round.
Why did we use priority 17? Every listener has a priority. The higher the priority, the earlier the listener will be executed. We want our custom LocaleListener to be earlier than the Kernel’s LocaleListener. According to Kernel events, The kernel LocaleListener has priority 16. Let us go abit higher, ie 17.
Now we need to create the LocaleListener class.
To see what is going on with the events sequencing,
Look at the kernel.request section and you should see our custom event listener ranked just above the kernel LocaleListener.
Can you use the AppSubscriber class that we have created to do the same job?
Now, clear the cache and refresh the browser. Try changing the locale dropdown and see for yourself.
Try changing the priority to 15 of kernel.event_listener tag and see what happens?
Update BDD (Optional)
Let us create the cest file based on the User Story.
now within the cest file:
Lets run the test to make sure everything is working.
Since the UI has been changed, some previous BDD tests might fail. Fix them and re-run the full BDD tests till everything passes.
Summary
In this chapter, we learned how to create translation files and updated the twig files to handle the translation. We have also created a language switcher in the admin area and added a new BDD test to test internalisation.
I am not french and my french translation might not be correct as I was using google translate. The use of french in this book is just an example.
Remember to commit all your changes before moving on to the next chapter.
Exercises
Remember all the twig files you have created in chapter 11? Update them to support i18n.
(Optional) Try creating translations in other languages other than french.
Our CMS should allow uploading of files. Let’s say we want to allow user to upload their own profile image. EasyAdmin has nice integration with a popular bundle called VichUploaderBundle.
Update User Stories
Let us update the user stories that we have created before.
Story ID 10.6: As an admin user, I want to manage all users, so that I can control user access of the system.
Scenario Id
Given
When
Then
10.6.1
List all profiles
I go show profile page” url
I should see a list of all users in a table with image fields
Story ID 10.1: As a test1 user, I want to manage my profile, so that I can update it any time.
Scenario Id
Given
When
Then
10.4.1
Show my profile
I go to show profile page
I should see test1@songbird.app and an Image field
10.4.5
Delete and Add profile image
I go to edit profile page And delete profile image and add a new image
I should see an empty profile, previous profile image gone and then a new one appearing in the file system.
10.4.6
Update profile image Only
I go to edit profile page And update profile image and submit
I should see user profile updated and previous profile image gone from file system.
Install Vich Uploader Bundle
Add the vich uploaded bundle to composer
In config.yml, we need to add a few parameters
and in Appkernel.php
We have to add new image fields to the user table.
Since we have changed the entity, we have to remember to log the db changes so that we can deploy the db changes to production easily.
Looks good, we can now reset the app
We can now verify that the new image field has been added.
We need to create the new upload folder
but we should ignore in git. In .gitignore
Update Fixtures
Let us update the Image field to help us with automate testing.
Just create any pic called test_profile.jpg and put it in the src/AppBundle/tests/_data dir. If you run out of ideas, you can use the jpg from my branch. We will update the resetapp script to copy the test_profile.jpg to the web folder.
Update UI
Let us update the UI to include the image field.
Let us resetapp, login and have a look
Update BDD (Optional)
In this chapter, we might need other modules like Db and Filesystem. Let us update our acceptance config file
and our db credentials
now run the build to update the acceptance library
You should now have lots of new functions to use in AcceptanceTesterActions.php.
Write the stories in this chapter as a practice. Again, get all the test to pass before moving to the next chapter.
Tip 1: To test a file upload, put a file under src/AppBundle/Tests/_data folder and you can then use the attachFile function like so
Remember to commit everything before moving on to the next chapter.
Summary
In this chapter, we have integrated VichuploadBundle with EasyAdminBundle. We made minor change to the ui and added new BDD tests.
A proper CMS needs a logging mechanism. We are talking about the admin area, not the front end. If something happens, we need to know what was done and what happened? We can log user activities in a file system but it is not very efficient. File system is good for logging errors - see monolog. Ideally, we need a database solution.
Define User Stories
After the user logs in, we want to record the username, current_url, previous_url, CRUD action, data on every page that the user visits. These data should be recorded in a new table. When the user is deleted, we do not want the logs associated with the user to be deleted, therefore the 2 tables are not related.
There is a popular loggable doctrine extension that we can use. However, it is easy enough to built one for ourselves.
15. Logging User Activitiy
Story Id
As a
I
So that I
15.1
admin user
want to manage user logs
check on user activity anytime.
15.2
test1 user
don’t want to manage user logs
don’t breach security
Story ID 15.1: As an admin, I want to manage user logs, so that I can check on user activity anytime.
Scenario Id
Given
When
Then
15.1.1
List user log
I click on user log on the left menu
I should see more than 1 row in the table
15.1.2
Show user log 1
I go to the first log entry
I should see the text “/admin/dashboard”
Story ID 15.2: As test1 user, I don’t want to manage user logs, so that I don’t breach security.
Scenario Id
Given
When
Then
15.2.1
List user log
I go to the user log url
I should get an access denied message
15.2.2
Show log 1
I go to the show log id 1 url
I should get an access denied message
15.2.3
Edit log 1
I go to the edit log id 1 url
I should get an access denied message
Implementation
We will create a new entity called UserLog. The UserLog entity should have the following fields: id, username, current_url, referrer, action, data, created.
Again, don’t memorise this command. You can find out more about this command using
In the entity, note that we are populating the username field from the user entity but not creating a constraint between the 2 entities. The reason for that is that when we delete the user, we still want to keep the user entries. We haven’t really gone through doctrine yet. You can read more about association mapping here if we want them to be related. We will touch on doctrine again in the later chapters.
Next, we will intercept the kernel.request event.
Let us create the new menu.
and the translation.
and the french version
Now reset the db, re-login again, click on the user log menu and you will see the new menu on the left.
There were db changes, let us capture the change so that we can update production when we need to.
and we can reset the db now.
Update BDD (Optional)
Let us create the cest files.
Tip: The assert module is very useful.
Let us add the assert module
Let us rebuild the libraries
Again, I will leave you to write the bdd tests. The more detail your scenario is, the better the test coverage will be. Get all the test to pass and remember to commit everything before moving on to the next chapter.
Summary
In this chapter, we created a new entity called UserLog and used the kernel request event to inject the required request data into the database.
Exercises
Modify the UserLog entity such that deleting the user in the User entity will delete the associated user entries in the UserLog entity. (optional)
What are the pros and cons of allowing CRUD actions on log entries?
Can you use doctrine loggable extension to achieve what was achieved here? (optional)
Can you implement automated entity logging using Traits?
Chapter 16: Improving Performance and Troubleshooting
If your site uses a lot of javascript and css, one good optimisation strategy is to merge the css and js into just one file each. That way, its one http request rather multiple, improving the loading time. There are also tools to find out where bottlenecks are and fix them.
Install Blackfire
Head to blackfire.io (another great product by sensiolabs) and sign up for an account. In https://blackfire.io/account, get the client and server (id and token). Enter them in .env.
We only need to configure blackfire.
Let us add the blackfire image to docker-compose.
and bring up the image
Upgrade ResetApp Script
./scripts/resetapp is a script that we invoke when we want to remove the cache and reset the database. It is often called if we make changes to the template or before we run test suites. To increase the efficiency of the script, we should allow user to specify if resetting the app requires deleting the cache or not as cache generation is an expensive process and the lag time can cause inconsistency in the tests.
What we need is a an optional switch to allow deleting or cache or not. Maybe even allow an option to load fixtures or not.
We will now use the “resetapp -c” instead to clear the db only when resetting tests.
Optimising Composer
We can also optimise composer by building an optimised class map to help speed up searching for namespaces. We can run this once during deployment to production.
Minimising JS/CSS
You might have heard of using assetic to manage assets and minimising JS/CSS from The book and The Cookbook. The nice thing about using assetic is that you can do compilation of sass or less files on the fly. If you are unsure about css preprocessor, I recommend checking them out. At the time of writing, sass is more popular.
The has been a lot of innovation in frontend technologies especially with node in recent years. gulpjs is being widely to minify js and css.
Assuming you are using mac, make sure you have homebrew. If not, install it
Install node if not done.
If successful, “node -v” and “npm -v” should return values. Now we create package.json.
Follow through the prompts. Then install bower.
Like npm, let us create the bower.json
Like before, follow through the prompts. Now, let us install all the bower dependencies.
Jquery and bootstrap are the 2 most widely used libraries. It make sense for us to include the libraries outside of AppBundle.
Let us install gulp and all the dependencies.
if everything is successful, we should see these new files and folders:
We only need the json files, we can put the bower_components and node_modules in .gitignore
package.json is important. We want the default node js file to be gulpfile.js. The package.json should look something like this:
Let us create the gulpfile.js
In short, this gulpfile.js simply says minify all relevant js and css, then copy the js, css and fonts to the web/minified directory.
Since we are only using 1 css and js file, we only need to include the files once in the base template.
We no longer need to use separate css for the custom views. Remove all the stylesheet blocks in src/AppBundle/Resources/FOSUserBundle/views/Resetting and src/AppBundle/Resources/FOSUserBundle/views/Security.
Let us update gitignore:
and create the minified dir
Since we are using bower to include common js and css, we can remove all the unncessary css and js that we have included from the previous chapters.
To compile the js and css, open up another terminal and enter
if you want to auto compile js or css files when you change the sass or javascript files
If everything is successful, you will see the new dir and files created under web/minified dir.
Now go to songbird.app/login, and verify the new javascript.js and styles.css are included by viewing the source code.
Troubleshooting
You should by now aware of the debug toolbar (profiler) at the bottom of the screen as you access the app_dev.php/* url. The toolbar provide lots of debugging information for the application like the route name, db queries, render time, memory usage, translation…etc.
If you have been observant enough, you should have seen the red alert on the toolbar. Try logging in as admin and go to http://songbird.app:8000/app_dev.php/admin/?entity=User&action=list and look at the toolbar. What happened?
You would see the obvious alert icon in the toolbar… Clicking on the red icon will tell you that you have missing translations.
There will be lots of “messages” under the domain column if there is no translation for certain text.
How would you fix the translation errors?
How about the performance link? What can you see from there?
Using the debug toolbar is straight forward and should be self explanatory.
Tip: PHP developers should be aware of the print_r or var_dump command to dump objects or variables. Try doing it with Symfony and your browser will crash. In PHP, use var_dumper and in twig, use dump instead.
Identifying bottlenecks with blackfire.io
Even though the in-built debug profiler can provide the rendering time and performance information but it doesn’t go into detail where the bottlenecks are. To find out where the bottlenecks are, we need Blackfire.
You should have installed blackfire from the previous section.
Once done, you should see a new blackfire icon on the top right of google chrome. Let us load the user management page:
and click “create a new reference”, then click on on the Profile button.
At this point, the chrome browser will interact with the php docker container and tells the blackfire agent to pass the diagnostic data over to blackfire server. You will also see some values in the blackfire toolbar. So we are talking about a few sec of processing time. This is slow and thats because we are using docker.
Once done, you will see a new profile toolbar. Give the profile a name, say “songbird prod default”.
Reverse Proxy and APCU
We will do another optimisation. Symfony comes with a reverse proxy and the ability to use apcu, let us enable it.
Now refresh the page and then click on the blackfire icon again. In the blackfire toolbar, compare it with the previous profile.
Did you see any improvements in the loading time. What was the improvement?
Click on “View comparision”
I was merely scrapping the surface of blackfire. I suggest you do the 24 days of blackfire tutorials if you want to dig in deeper.
Fix Coding Standards with PHP-CS-Fixer
PHP-CS-Fixer automatically fixes coding standards. Its always a good idea to use it to clean up your code before commiting.
once this php-cs-fixer is installed, we can use it from the command line like so
Let us add the php-cs-fixer cache dir to .gitignore as well
Do it for the src directory as well. Run all the tests. We can now commit all the fixed files once we are happy with the results.
Summary
In this chapter, we briefly discussed several optimisation strategies. We installed blackfire, minified css and js using gulpjs. We have also refactored the runtest script so that it doesn’t clear the cache every time it starts a new test. lastly, we walked through troubleshooting using the web toolbar and blackfire.io.
Exercises
Using the debug profiler, fix all the translation errors.
What other performance enhancing tools can you think of?
So far, we have been very lazy (a good thing?). We have offloaded bulk of the CMS functionality to FOS and EasyAdmin bundles. In this chapter, we will create a simple reusable page bundle and the bulk of the logic ourselves. Let us call this NestablePageBundle.
The Plan
We want our page bundle to have no dependency on other bundles like FOSUserbundle. Each page should have a unique slug and a couple of meta data such as title, short description, long description, created_date…etc. We will be using nestable js to allow drag and drop + page nesting using ajax.
We will create 2 entities. The first entity is the Page entity and will consist of simple attributes like id, slug, sequence, parent and children id…etc. The second entity will be the PageMeta entity consisting of attributes like name, locale, title, short description and content. The relationship between the Page and PageMeta entity will be one to many.
This bundle creation is for illustration only and has lots of rooms for improvement.
Define User Stories
Since SongbirdNestableBundle is going to be decoupled from AppBundle, so we will need 2 sets of tests, one for SongbirdNestableBundle and one for the AppBundle. We will worry about the AppBundle tests in the next chapter.
SongbirdNestableBundle
Story Id
As a
I
So that I
17.1
test2 user
want to manage pages
can update them anytime.
Story ID 17.1: As test2 user, I want to manage pages, so that I can update them anytime.
Scenario Id
Given
When
Then
17.11
List Pages
I go to /songbird_page
I should see the why_songbird slug under the about slug
17.12
Show contact us page
I go to /songbird_page/5
I should see the word “contact_us” and the word “Created”
17.13
Reorder home
I simulate a drag and drop of the home menu to under the about menu and submit the post data to /songbird_page/reorder
I should see “reordered successfully message” in the response and menus should be updated
17.14
Edit home page meta
I go to edit homepage url and update the menu title of “Home” to “Home1” and click update
I should see the text “successfully updated” message
17.15
Create and delete test pagemeta
go to /new and fill in details and click “Create” button, then go to test page and click add new meta and fill in the details and click “create” button, then click delete button
I should see the new page and pagemeta being created and pagemeta deleted
17.16
Delete contact us page
go to /songbird_page/5 and click “Delete” button
I should see the contact_us slug no longer available in the listing page. Page id 5 should no longer be found in the pagemeta table.
Create Our Own Bundle Generation Script (Optional)
The default bundle generation script is cool. Let us customise it further to make our life easier. We will create a custom script to generate songbird bundles.
now let us run the script
run a git status and you will see that the script does a lot of work for you.
Implementation
Let us create the entities.
For Page entity:
and for PageMeta entity:
We now need to update the relationship between the 2 entities:
and
There were some new doctrine association annotations used here, notably @ManyToOne and @OneToMany are the most common. Establishing the right associations can save lots of time when managing table relationships. For PageMeta.php, we set the default locale to “en” if none is set.
We can now auto generate the stubs for the 2 entities:
This command helps us to generate the getters and setters for the new variables that we have added. For the page entity for example, you should see new functions like setParent() and getParent() being added - another huge time saver.
We will also create a helper to help us find the page meta entries based on locale.
Before we reset the app, let us create the doctrine migration file so that we can deploy this db changes to production (if we have one). It is a good practice to do that.
reset the app now and verify that the 2 new tables, ie page and page_meta being created in the songbird db.
We are going to use a variant of nestable.js to create our draggable menu. Let us create the js and css directories.
Download jquery.nestable.js and put it under src/Songbird/NestablePageBundle/Resources/public/js/jquery.nestable.js
Now let us create the css
Let us now create the translation files.
The english version:
and the french version:
We will now generate CRUD for the 2 entities in a quick way:
Noticed we use “g” as a shortcut to “generate” in the command line. We’ve added the route-prefix to make sure our path is unique so that it can be reused with minimal changes.
Create Sample Data
Let us populate sample data to work with. Say we want 3 parent menu, Homepage, “About Us” and “Contact Us” and a couple of submenus.
reset the app to load the fixtures and check that the entries have been added to the db.
Now go to the page url and you should see the default crud template
Everything is looking plain at the moment, let us integrate nestablejs.
Integrating NestableJS
How do we integrate NestableJS to our bundle? The secret will be in the Page Controller. We will change the logic there.
We have added 2 extra methods, listAction and reorderAction. As the controller should have minimum logic, we have moved the bulk of reorderAction logic to the repository.
We need a custom query to get the pagemeta based on locale.
We then remove the created and modified date from the form as these fields should not be editable.
and we will leave the PageMetaController.php as default.
Now, we need to make changes to the view - list.html.twig.
and tree.html.twig
We need to update the show.html.twig to allow user to view pagemeta.
The rest of the view templates can use the defaults. Ready to test the bundle?
Now go to the page index and try reorder the menu.
Create Functional Tests (Optional)
Sticking to the industrial standard, we are going to use PHPUnit rather than codeception. The main reason for doing that is to remove dependency on codeception. The only downside is that we could not simulate real browser interaction with the app.
Let us install phpunit using composer
We need to call phpunit in docker, so we need another wrapper for it.
Then, let us create the functional tests based on the user stories.
phpunit uses the phpunit.xml.dist under the symfony dir. To run the test, simply run this command in the symfony dir
let us run testListPages function within PageControllerTest.php for example,
You should get a “no tests executed” error because we haven’t write the test. Let us write the test.
As we are testing both page and pagemeta controller at the same time, we can remove the pagemeta controller test.
lets run the test again and make sure everything is ok
You might have noticed that the phpunit functional tests seemed to run much faster than codeception acceptance tests. Why? Does that makes it more attractive to you?
Whatever we do in this chapter should not affect what we have done previously. To verify that this is indeed the case,
Remember to commit all the code before moving on.
Summary
In this chapter, we have created our own page bundle and generated CRUD in a quick way using the command line. We have also customised the listing page and created a draggable menu using the jquery nestable menu. Data is submitted to the backend via ajax and updated dynamically.
Exercises
Are there any benefits of creating a page bundle that has no dependency on Symfony at all? How would you do it? (Optional)
KnpmenuBundle is a popular bundle for handling menus. How would you integrate it with SongbirdNestableMenu? (Optional)
We have created a page bundle in the previous chapter using the default way. It’s not perfect if you want to share it with everyone. How do we do that? Be warned, we need lots of refactoring in the code to make it sharable.
This is a long chapter. Its is a good process to go through because it makes you pause and think. If you already know the process and want to skip through, simple clone the NestablePageBundle from github and follow the installation instructions in the readme file. Then, jump over to the next chapter.
Creating a separate repository
First of all, let us create a readme file.
Update the readme file.
Let us create the composer.json file for this repo. We will do a simple one
Follow the prompts. You might need to read up on software licensing. MIT license is becoming really popular. The sample composer.json might look like this:
Note that we have to add the “autoload” component so that Symfony can autoload the namespace post installation. PS-4 is the default standard at the time of writing. Next, let us create the license in a text file
In github (create a new acct if not done), create a new repo. Let’s call it NestablePageBundle for example. Once you have created the new repo, you should see instructions on how to push your code.
Let us give our first release a version number using the semantic versioning convention.
Your repository is now available for the public to pull.
Updating Application composer.json
If we add our repo to packagist, we could install our bundle like any other bundles using the “composer require” command. Anyone reading this tutorial might submit their test bundle to packagist, so I thought it would be a better idea to install the bundle from git instead. Let’s use github for the sake of illustration.
Note that the bundle name is “nestable-page-bundle” under the “require” section. Why not use NestablePageBundle following Symfony’s convention? Remember the composer.json file that you have created previously? “nestable-page-bundle” is the name of the bundle as specified in that composer file.
Now lets run composer update and see what happens
At this point, look at the vendor directory and you will see your bundle being installed in there. That’s a good start.
Renaming SongbirdNestablePageBundle
Let us do some cleaning up. We no longer need the src/Songbird/NestablePageBundle since we have installed the bundle under vendor dir.
Let us check if the route is still there.
Woah!! We have already deleted src/Songbird/NestablePageBundle and we should expect to see some errors. Why are the songbird routes still there?
We have a problem. The namespace “Songbird” is no longer relevant in vendor/your-name/nestable-page-bundle since the bundle is already decoupled from Songbird CMS. We want to change the bundle’s filename and namespace so that it is more intuitive. How do we do that?
Let us re-download the repo and do some mass restructuring
There is no quick way for this, some bash magic helps
That should save us 90% of the time. Then visually walk through all the files and rename whatever that was not renamed by the bash commands.
Lastly, rename the bundle file
Now, here is the question. How do we test our changes without committing to git and re-run composer update? We can update our entry in vendor/composer/autoload_psr4.php
Now, let us update AppKernel
and routing
My initial is bpeh, let us check that the routes are working.
We can now install the assets.
Now go your new page list url and do a quick test. In my case,
Looks like it is working. How can we be sure? Remember our functional tests?
If it fails, why? Can you fix it?
Remember to commit your code before moving to the next chapter. Up your nestablepagebundle tags to 0.2.0 or something else since there were major changes.
Making the Bundle Extensible
When this bundle is initialised in AppKernel.php, running “scripts/console doctrine:schema:create will create the default tables. We should be able to extend this bundle and modify the entity name and methods easily. The war is not over. There are still lots to be done!!
Let us clean up the AppKernel and Route.
and in routing.yml
and refocus our attention to the NestablePageBundle:
First of all, we need to make Page and PageMeta entities extensible. We will move the entities to the Model directory, making the entities abstract.
I’ll be using my initial “bpeh” from now onwards to make life easier when referencing paths.
Note that we have changed all variables to “protected” to allow inheritance. The references to PageBase has also been changed.
To make our bundle flexible, we also need to allow user to specify their own child entities, form type and templates to use.
and the extension
Now in config.yml, anyone can define the page and pagemeta entities themselves.
We also need to run the constructor to initialise the new config parameters when the controllers are loaded. To do that, we will need to do it via the controller event listener.
and in the controller listener class
The Page Controller can now use the parameters as defined in config.yml to load the entities and form types.
Likewise for PageMeta Controller
We also need to refactor PageMetaRepository because findPageMetaByLocale can now return either an object or scalar value.
There are other stuff to be done
Create the translations.
Move all the related views from app/resources/views to vendor/bpeh/nestable-page-bundle/views
Update functional tests.
Once you are happy with it, give it a new tag and commit your changes again.
The bundle is now ready to be extended.
Extending BpehNestablePageBundle
To make things easy, I’ve created a demo bundle and you can install the demo bundle and test out it for yourself.
Let us extend BpehNestablePageBundle by copying the PageTestBundle.
Let us call this bundle PageBundle to keep it simple.
Let us configure the Entities
and PageMeta.php
Let us update the PageRepository.php
and PageMetaRepository.php
Let us update the PageController to have a route which is easier to use
Now PageMetaController.php
Its time to update the basic forms
and PageMetaType.php
Let us confirm the new routes are working…
Looks good. Its time to update config.yml
Remember to clean up the routes.
Init the new bundle in AppKernel.php
Remember to update the data fixtures.
There were schema changes. We have to update the sql so that we can deploy it easily if we need to. stash our work and load chapter_16 db.
Reset the db again.
Now go to http://songbird.app:8000/app_dev.php/page and make sure the new url should be working.
run all the tests and make sure you didn’t break anything.
I hope you are getting used to this… Its a pretty routine process once you get used to it.
Summary
In this chapter, we have created a new repo for the NestablePageBundle. We have updated composer to pull the bundle from the repo and auto-loaded it according to the PSR-4 standard. We learned the hard way of creating a non-extensible bundle with the wrong namespace and then mass renaming it again. Making the entities extensible was a massive job and required a lot of refactoring in our code. If you know you are creating a reusable bundle, its better to get the namespace correct and create it right from the start.
We have done so much to make NestablePageBundle as decoupled as possible. Still, there are lots of room for improvement. Was it worth the effort? Definitely! People can now install our bundle in their Symfony applications easily.
Exercises
Delete the whole vendor directory and try doing a composer update. Did anything break?
In this chapter, we are going to integrate NestablePageBundle with EasyAdminBundle. We are also going to improve the cms by integrating a wysiwyg editor (ckeditor) and create a custom locale dropdown.
Define User Stories
19. Page Management
Story Id
As a
I
So that I
19.1
an admin
want to manage pages
update them anytime.
19.2
test1 user
don’t want to manage pages
don’t breach security
Story ID 19.1: As an admin, I want to manage pages, so that I can update them anytime.
Scenario Id
Given
When
Then
19.11
List Pages
I go to page list url
I can see 2 elements under the about slug
19.12
Show Contact Us Page
I go to contact_us page
I should see the word “contact_us” and the word “Created”
19.13
Reorder home
I drag and drop the home menu to under the about menu
I should see “reordered successfully message” in the response and see 3 items under the about menu
19.14
edit home page meta
I go to edit homepage url and update the menu title of “Home” to “Home1” and click update
I should see the menu updated to home1
19.15
Create and delete test page
go to page list and click “Add new page” and fill in details and click “Create” button, go to newly created test page and create 2 new test meta. Delete one testmeta and then delete the whole test page
I should see the first pagemeta being created and deleted. Then see the second testmeta being deleted when the page is being deleted.
19.16
Delete Contact Us Page
go to contact us page and click “delete”
I should see that the contact us page and its associate meta being deleted.
19.17
Create new page with existing locale
go to page list and click “Add new pagemeta” and fill in details, select locale as en, page as home and click “Create” button
I should see an exception.
Story ID 19.2: As test1 user, I don’t want to manage pages, so that I don’t breach security.
Scenario Id
Given**
When
Then
19.21
List pages
I go to the page management url
I should get a access denied message
19.22
show about us page
I go to show about us url
I should get a access denied message
19.23
edit about us page
I go to edit about us url
I should get a access denied message
19.24
List pagemeta
I go to list pagemeta url
I should get a access denied message
Adding new image field to PageMeta Entity
Let us add a new field called featuredImage to the PageMeta entity. We will configure Vich uploader to do the job.
Let us update config.yml
Installing CKEditor
We will now install CKEditor
then enable the bundle
Integration with EasyAdminBundle
There is still some effort to get BpehNestablePageBundle integrate properly with EasyAdminBundle. The reason is because the big difference in controller logic between the 2 bundles.
Let us assume that we not going to use the PageController.php and PageMetaController.php except the reorder route
The new routing.yml as follows:
We now need to add actions to the AdminController. The new AdminController should look like this:
Let us create the list view. We have to recreate it because we are extending it from the easyadmin layout instead.
and the contents of list.html.twig
```
# app/Resources/EasyAdminBundle/views/Page/list.html.twig
Noticed the new field type we have used, ie ckeditor, vich_image, and AppBundleFormLocaleType. EasyAdminBundle has internal support for ckeditor and vich_image but AppBundleFormLocaleType is our own custom form selector which will be discussed in the next section.
Creating Custom Locale Selector Form Type
If you are looking at the pagemeta page, say http://songbird.app:8000/app_dev.php/admin/?entity=PageMeta&action=new for example, you should have noticed by now that user can enter anything under the locale textbox. What if we want to load only the languages that we defined in the config file (ie, english and french)? It is a good idea to create our own dropdown.
The array localChoices is passed into the constructor. This class can be lazy loaded if we define it in service.yml
See how we pass the supported_lang config variable into the class? Now, go to any pagemeta new or edit page (ie http://songbird.app:8000/app_dev.php/admin/?entity=PageMeta&action=new for example) and you should see the locale dropdown updated to only 2 enties.
Let us update the translation files
and the french version
There were db changes. remember to run doctrine migrations
Update BDD Tests (Optional)
Let us create the cest files,
Create the test cases from the scenarios above and make sure all your tests passes before moving on.
Remember to commit all your code before moving on to the next chapter.
Summary
In this chapter, we have extended our NestablePageBundle in EasyAdmin. We have installed CKEditor in our textarea and created a customised locale dropdown based on values from our config.yml file. Our CMS is looking more complete now.
Exercises
From the debug toolbar, update the missing translations.
TinyMCE is also a widely used WYSIWYG editor. How do you integrate it in Sonata Media?
What if you want to add a new user field to the Page Management System? What is going to happen to the page if the user is deleted?
Can you make inserting pagemeta easier for every new page added? This just shows how much thought one person need to put when creating a software.
Going to “http://songbird.app:8000/” has nothing at the moment because we have so far been focusing on the the admin area and not touched the frontend. In this chapter, we will create an automatic route based on the slug and display the frontend view when the slug matches. Any route that matches “/” and “/home” will be using the index template while the rest of the pages will be using the view template.
We will create a simple home and subpages using bootstrap and use smartmenus javascript library to create the top menu which will render the the submenus as well.
Lastly, we’ll add a language toggle so that the page can render different languages easily. The menu and page content will be rendered based on the toggled language. To get the menu to display different languages, we will create a custom twig function (an extension called MenuLocaleTitle).
Define User Stories
20. Frontend
Story Id
As a
I
So that I
20.1
test3 user
want to browse the frontend
I can get the information I want.
Story ID 20.1: As test3 user, I want to browse the frontend, so that I can get the information I want.
Scenario Id
Given
When
Then
20.11
Home page is working
I go to the / or /home
I can see the jumbotron class and the text “SongBird CMS Demo”
20.12
Menus are working
I mouseover the about menu
I should see 2 menus under the about menu
20.13
Subpages are working
I click on contact memu
I should see the text “This project is hosted in”
20.14
Login menu is working
I click on login memu
I should see 2 menu items only
Creating the Frontend
Let create a new frontend controller
All routing magic can be done with the @Route annotation (we can even use regex as shown in the indexAction). With the new @Template annotation, the action just need to return an array rather than a response. With the new routes added, we will move the frontend routes to the last priority, so routes like /login will be executed first.
Let us update the frontend base view.
We will now create a homepage view.
and pages view
and lastly, recursive view for the menu
Note the new getMenuLocaleTitle function in the twig. We will create a custom function usable by twig - Twig Extension.
we now need to make this class available as a service.
Since we have added a new top navbar, let us remove the SongBird logo from the login and password reset pages. Update the following pages as you see fit:
Let us update bower.json to pull in smartmenus js.
then make gulp to pull the libraries in
Let us update the datafixtures as well.
I’ve added new images to the homepage. The new images are in the src/AppBundle/DataFixtures/ORM/images folder. Feel free to get the images from there.
Lastly, let us update the stylesheets. We might as well update them in scss
We no longer need our old .css files
Now run gulp and refresh the homepage and everything should renders.
Remember to create the featured_images dir and reset the db if not done
Go to homepage and this should be the end result.
Update BDD (Optional)
Let us create the cest file:
Write your test and make sure everything passes.
Summary
In this chapter, we have created the frontend controllers and views. We used smartmenus to render the menus and converted our css to sass. Finally, we wrote BDD tests to make sure our frontend renders correctly. The CMS is now complete.
Exercises
Try extending the NestablePageBundle so that you can have multiple menus, say a top and bottom menu?
One of the argument against using a language toggle is that it is bad for SEO. Language toggle can be good for usability. Can you think of a way to overcome the SEO issue?
Upon reflection of what we have covered in the last 20 chapters, I think there are lots of improvements that can be done. In particular, I feel that I wouldn’t do justice to this book if I don’t give an example of Compiler Pass.
This is an advance chapter. If you skipped all the chapters and came to this chapter by chance, I recommend you to read up DI and DIC before continuing.
In this chapter, I like to introduce 2 improvements to the CMS.
a) Simplifying config.yml
b) Adding Simple User Access Control to EasyAdminBundle.
Simplifying config.yml
Due to DI, the bundle Extension is called when the bundle is being initialised. The end result is a bunch of parameters and services that can be used and referenced throughout the application.
The app/config/config.yml is read by all bundle extensions so that relevant information relating to the bundle can be extracted. So far, there are many configuration parameters like fos, vich, doctrine …etc. To make the installation easier, we could move all these extra configuration to elsewhere so that developers don’t have to worry about them when installing the CMS and it also makes the file looks cleaner.
Noticed that I could have moved more parameters over to the prepend function if I want to simplify the installation further.
Adding Simple Access Control to EasyAdminBundle
I still want to comment javiereguiluz for creating the wonderful EasyAdminBundle. As of current, the bundle doesn’t support user permissions out of the box. I believe there might be plans to include this feature in the future as it is a widely requested feature.
As an exercise, let’s say that we want to customise the bundle so that we can control access to certain parts of the admin area based on the user’s role and we want to do that simply by changing the easyadmin yaml files.
Let us allow all authenticated users to access the admin area rather than just ROLE_USER.
The new design.yml should look like this:
Noticed that we have added a new attribute called “role” to each menu item and the value (say “ROLE_ADMIN”) means the mimimum permission level required to access that menu. In this case, everyone can see the dashboard, ROLE_USER and above can access the User link and only ROLE_ADMIN can see the User and UserLog link.
We are going to do something similar for all the entities yaml, starting from the page entity
Now, the user entity:
and finally - userlog.yml.
When parameters and services are created by the extension but not yet compiled in optimised DIC, there is a chance to manipulate them. Compiler Pass exists for this purpose.
Let us tell our AppBundle to initiate its compiler pass when it is loaded by the kernel.
We have added a new compiler pass class called ConfigPass.php. Compiler Pass needs to extend the CompilerPassInterface.
What we have done here is to change the easyadmin.config parameter produced by the EasyAdminBundle. easyadmin.config is simply a bunch of arrays built based on the yaml config under app/config/easy_admin. Each for-loop adds a new key called “role” with the default “IS_AUTHENTICATED_FULLY” role if not specified by the config.
EasyAdmin dispatches lots of events. We were already subscribed to it.
We now need to add a bit more logic to the subscriber.
We have triggered the checkUserRights function based on a few EasyAdmin events. We have allowed the logged in user to edit his own profile irregardless of role’s permission. Then, the for-loop does the magic of allowing or denying user to access different parts of the admin area based on the role key in easyadmin.config.manager service.
Note that this will work only if our AdminController dispatches the events, ie
The menu display is not managed by the event subscriber. We have to add an is_granted statement before rendering the menu. See below:
Try logging in now as test1 and you will see that the menu and entities should be access controlled.
Adding Roles to EasyAdmin Actions
We have seen that easyadmin actions is controlled by the yml files, ie something like:
What if we want the actions to be “role” aware? If you look at the easyadmin twig files, you will see that it calls a Twig function “getActionsForItem” to get the actions prior to render. This gives us a chance to change the function logic by extending the class.
And we have to remember to call our new twig class in services.yml
One thing to remember though is that we have to load our AppBundle after EasyAdminbundle so that our app.twig.extension can override the easyadmin.twig.extension service of EasyAdminBundle
I have disabled the “edit” action for all users, so the edit button will not show even if the user is himself. For the sake of simplicity, let us change the layout header link to use edit action instead.
Cleaning up
We are close to the end of the chapter. Let us clean up all our code using php-cs-fixer (Still remember this?)
Update BDD (Optional)
We have updated some business rules. Users can now see and do what they are allowed in the admin area based on their role in the easyadmin yaml config files. Its time to ensure we update our tests to reflect these changes.
Summary
In this chapter, we have cleaned up config.yml and provided a custom solution (Using compiler pass and event listeners) to make EasyAdmin support user permissions in the admin area. It was a huge effort but not yet a full solution. However, it should make life easy for people who wants to configure admin permissions easily.
Exercises
Think of another way to make EasyAdmin support user permissions.
Write your test and make sure everything passes (Optional)
Can you implement autowiring in services.yml? What are the pros and cons of using autowiring?
Congratulations for perservering for so long… It’s been a long journey. In the previous chapters, we have created a simple CMS using a modular approach. The CMS is really simple but is secure, supports user logging and internalisation. While going through the exercises, we have explored possibilities to build different parts of the CMS bit by bit.
Now, you have all the basic knowledge and foundation to create more complex applications with Symfony.
So, what’s next from here? Ready for more adventures?
Here are some suggestions:
Start building something with Symfony fullstack or its components.
I am sure you will find bugs and typos along the way. Create pull requests for SongBird in git.
Improve on the NestablePageBundle to reduce the amount of work required to integrate with EasyAdminBundle.
Create API for 3rd party services to connect to.
Add Ecommerce capability to the CMS by adding a payment module.
Improve on look and feel. The frontend looks too plain.