Stars: 113
Forks: 31
Pull Requests: 150
Issues: 62
Watchers: 11
Last Updated: 2023-07-04 16:05:18
A simple app skeleton to try to make every components work together : symfony 5.* (latest stable at the date, but work with sf 4 and 3.3+ if you pull the right tag), symfony/flex, webpack-encore, vuejs 2.5.x, boostrap 4 sass
License: MIT License
Languages: CSS, JavaScript, Vue, PHP, HTML, TypeScript, SCSS, Twig
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Rebolon/php-sf-flex-webpack-encore-vuejs/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Rebolon/php-sf-flex-webpack-encore-vuejs/badges/quality-score.png?b=master) [![DeepScan grade](https://deepscan.io/api/teams/2301/projects/3192/branches/26485/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=2301&pid=3192&bid=26485) [![Known Vulnerabilities](https://snyk.io/test/github/rebolon/php-sf-flex-webpack-encore-vuejs/badge.svg?targetFile=package.json)](https://snyk.io/test/github/rebolon/php-sf-flex-webpack-encore-vuejs?targetFile=package.json) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FRebolon%2Fphp-sf-flex-webpack-encore-vuejs.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FRebolon%2Fphp-sf-flex-webpack-encore-vuejs?ref=badge_shield) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FRebolon%2Fphp-sf-flex-webpack-encore-vuejs.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FRebolon%2Fphp-sf-flex-webpack-encore-vuejs?ref=badge_shield)
You need PHP (7.4+), composer, node (15+ is better) npm or yarn and @angular/cli package installed globally
You also need to configure your php with curl and openssl
You have to setup the certificates download pem file, put it somewhere on your system and set your php.ini with those values:
You can run the app with a PHP Built_in server (all npm command relys on it), but some features may not work finely. In fact, Api-platform package rely on some HTTP SERVER features that are not implemented by the PHP Built-in server. So the api indexes like /api/index.json, /api/index.jsonld or /api/index.html will just return a 404 Not Found. There is also the react-admin app generated by the Api-platform that will not work because it relies on api json index.
To run the tests:
PANTHER_CHROME_DRIVER_BINARY=
This application has been realized to get a sample front app with sf4+ & vuejs but it also shows basic controllers with what most developpers do : basic controller, controller with twig, http call to external API, logging, API... We will try to not use front manipulation outside of VueJS (the sample with twig are really basic and won't use form per example) Here is how it has been created:
Then some php controllers has been created on following routes :
And then we followed the documentation here from api platform to create the Admin React tool. We just proceed to some adjustments:
But, Vuejs, ReactJS and Angular together ? with Symfony4, WTF ??? Yes it can seems completely stupid to use all this technologies together, but don't forget one thing : this is a POC ! The aim is not to help you to mix all those techs, but just to help you to use some of them finely. The biggest problem in my case is the dependancy management : all those JS libraries may need the same deps but in different version... For instance it seems to be ok, but i think that in future it could be a real brain-teaser.
You can change the php executable (if you want to use a php version with xdebug, or just another version of php) using package.json in the config section. Default php uses the one in path if exists. You can also change the web server port and the asset server port in the same config section.
"config": {
"php": "php",
"server_port_web": "80",
"server_port_asset": "8080",
"test_browser": "chrome:headless,firefox"
},
The test_browser section represent all the browsers you want to use with the Panther testing tool (we previously used testcafe, but it changes toot much and results was not so satisfying).
php bin/console security:check
Here are some non installed components that may help you:
For Angular (v5), i decided to do quite different way:
yarn add -g @angular/cli
) like this: cd assets/js && ng new devxpress-angular && cd devxpress-angular && yarn installencore
will remove dist folder when we run some command so with different dist folders i don't have any problemnpm run serve
but npm run build
with watch options because this last one is the only way to generate files in dist-ng folder. ng serve does everything in memory onlygit clone [email protected]:Rebolon/php-sf-flex-webpack-encore-vuejs.git
and go into this directorynpm run init-project
(or init-project:w for windows system) which will launch :
cp .env.dist .env
composer install
npm install && cd assets/js/form-devxpress-angular && npm install
npm run dev
php bin/console doctrine:database:create
& php bin/console doctrine:schema:create
& php bin/console doctrine:fixtures:load
config/packages/doctrine.yaml
filesrc/Entity/*
filescalibre
software plus a plugin that export data to sqlite format. An alternative to this would have been to use https://github.com/hautelook/AliceBundle and build yaml fixtures, but i already had an sqlite db so i didn't need this :-)npm run jwt-init
and use the passphrase you setup for JWT_PASSPHRASEChange to the project directory
Execute the npm run build
that will build assets and watch for angular app change (if you have a node-sass error Node Sass does not yet support your current environment...
, you may need to run npm rebuild node-sass
)
Execute the npm run dev-server-hot
(or dev-server-hot:w for windows system) command to start the asset server that will build your assets for vue and react and your manifest.json and serve the assets with hot module replacement when you do a modification on a vuejs file
Execute the npm run sf-dev
(or sf-dev:w for windows system) command;
Browse to the http://localhost:80/ URL.
php bin/console server:start 127.0.0.1:80
Run frontend tests with npm run test
Don't forget to prefer an nginx/apache server to be able to use full features of api-platform.
If you want to change default ports, you can use package.json > config : server_port_web for the web server (php built in server), and server_port_asset for the asset server. Default ports are 80 and 8080.
If you update the project:
doctrine:migrations:migrate
to take care of DB modifications.npm run dump-js-config
(or dump-js-config:win for windows system) to create the js config fileTo make it easy to test your API, i added a file POSTMAN.json that you can import into Postman cli. Then just configure your postman environement with the rights variable (the host...). Then run the login route, copy the bearer into your postman environment variable and then you will be able to test all protected route.
Everything is managed by 'encore' symfony package, so have a look at the webpack.config.js and then read their docs
Take care, the asset server listen to port 8080 so don't start your main server on that port, or specify another port for the dev-server using --port 9999
for example
Also, if you want to use the asset server finely, you have to add the assets configuration in the config/packages/framework.yaml file :
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
. In fact the npm command will build asset in memory only, and modify the manifest file to map asset to a new url served by the asset server instead of the main web server.
In the main layout, we load 3 common files: manifest.js, vendor.js and service-worker.js. Vendor is where you wan put all common libraries used on almost all pages. The source file for this bundle is assets/js/app.js. SW is for the service workers. It's default behavior is to manage the Cache file. You can have a look at offline-plugin for webpack.
The project uses 2 packages to lint and fix the code style of PHP code :You can install phpcs to check your code
Lint with this command vendor/bin/phpcs src -n --standard=PSR1,PSR2 --report=summary
to get a summary of errors.
You can fix it with vendor/in/phpcbf src -n --standard=PSR1,PSR2
which is delivered with phpcs, but it doesn't fix everything
so i suggest to use php-cs-fixer with this command vendor/bin/php-cs-fixer fix src --rules=@PSR1,@PSR2
For Javascript the following packages has been used:
npm install prettier
To lint the code: node bin-prettier.js assets/js/**
To fix it: node bin-prettier.js assets/js/** --write
For PHP you should configure your IDE to follow PSR1/PSR2 code style (or anything else if you prefer). For JS you will have to install prettier tool.
On PHP we use PHPUnit and Symfony web testcase. We have basic tests on PHP that will try to test that we have HTTP 200 OK on each routes. We should also tests Commands and other classes, but we should also test more finely the API content. Take care with Symfony4 to configure the config/packages/test/framework.yaml file to overload the session.storage_id with a mock !
JWT Certificates in test mode, we don't use pem files from your dev/prod env. In fact we use pem files generated specifically for tests and copied from var/travis/config to var/cache/config
On Javascript we have unit and e2 tests. Units tests are managed by jasmine and karma. It allows to test function, class, component.
For e2e tests we use testcafe from devExpress. It allows to launch browsers and drive them by code to reproduce a human behavior.
Here the tests runs on a chrome headless, and firefox but you can configure it in the package.json file in the config.test_browser node.
We now use Panther to run e2e tests. It's automatically ran when we run phpunit commands.
npm run test-karma
will run js test and will generate a karma_report.xml files in the following folder /var/report/
.
If you wonder how to tests your VueJS components, you can hae a look at this website which describe a lot of tests. Sadly it's in french !
In this application i used Karma for one application with VueJS (in /assets/js/vuejs). And i used Panther to test pages fully generated by Symfony. Karma is configured with assets/tests/units/karma.conf.js !
On PHP i use those 2 packages to prevent the use of deprecated packages or with vulnerabilities:
php bin/console security:check
it will checks your dependancies vs known vulnerabilites. You should use it on existing project.On JS i use snyk services.
@TODO finish on PHP and JS checks + tools to audit the code + software that analyse sql/xss/file injection, csrf, ... @TODO explain the usage of tools like OWASP ZED, sqlmap, php avenger... @TODO help to setup security system: stateful app = take care at csrf ; stateless app = should i use jwt, api key, OAuth, anything else ?
Don't forget to use HTTPS, even in local to help you find errors that will happen in production. One certificate has been generated for localhost (with http://www.selfsignedcertificate.com/) and is available in /var/certificates/.cert|.key There is a simple nginx conf (used for travis CI) that use those certificates so you can use nginx to work (just don't forget to change the port that is fixed to 80 like setup in the package.json). Travis will no more be supported in 2021 because they change their business model. I may move on gitlabCI in future.
TestCafé for functional testing generate an error when you don't use ssl: Uncaught (in promise) DOMException: Only secure origins are allowed (see: https://goo.gl/Y0ZkNV). But for instance i didn't found any solution to run it finely without --skip-js-errors parameters.
In Symfony i configured different firewalls:
If you doesn't need JWT, you can use ApiKey pattern. For this you have to implement the required Authenticator: https://symfony.com/doc/current/security/api_key_authentication.html If you need more tuning with huge APIs, you may require OAuth with JWT. OAuth will help you so have a look at this website: https://oauth.net/
In statefull app you will have HTTP Cookies. Even if you don't really manipulate them, and that they are used only to transport the sessionId between the server and the client. But if you use them to store other informations, you should be aware that they can be misused and that they can open XSS or CSRF web fail. Your cookies must have some specific attributes:
You have to pay attention at them. And you can play with them on those sites: http://cookies.rocks and http://example-bar.com (source here: https://github.com/hsablonniere/cookies.rocks and related talk here: https://github.com/hsablonniere/talk-back-to-basics-cookies)
Take care at the custom listeners that you could write based on Api-Platform documentation. They are used by all controllers, not only those from ApiPlatform.
In /api you can test the API with the swagger interface. Because the route uses JWT token you have to call the /demo/security/login/jwt/tokens route with HTTP POST and Basic Auth. Login is test_php or test_js and password is test ! Then in the swagger interface click on the upper right button Authorization and add 'Bearer THETOKENRECEIVED'
To get a valid JWT token you can use the lexik command : php bin/console lexik:jwt:generate-token
You can also provide a username/password to the command, i let you read the help of the command to know how-to do.
When using GraphiQL with secured resources, you won't be able to provide any token (i didn't find any clue for this). So i found this good tool to query my API by providing required Authorization header: https://altair.sirmuel.design/.
If you want to allow sorting based on columns, you will have to add Filter annotations on Entity. Look at the Book entity and its ApiFilter which allow to sort on id and title. Then you will be able to call the api like this: http://localhost/api/books?order[id]=DESC
Sometime you will want to filter on some fields: i want Reviews published after xxx. For things like this you have to add new ApiFilter (like we did with sort). There is a sample in the Review entity
You can also filter on relation, but in that case you will have to use the id (or the iris) of the relation as the value of the filter. More information in documentation of ApiPlatform.
At the beginning when you query a Books route, each nested entities like Editors, Series, Authors will be represented only by IRI like : "/api/serie/1" This is cool when you don't need all nested information, you will prevent the user to download too many data. But very often, your application will require those informations, and in that case you seem condamned to do further HTTP call to retrieve all sub entities.
You are on the wrong way, and i was too !
Test the route /api/books
and you will retreive that kind of response:
[
{
"id": 1,
"title": "20th Century Boys (Deluxe), Tome 1 test",
"description": null,
"indexInSerie": 1,
"authors": [
{
"id": 1,
"role": {
"id": 1,
"translationKey": "JOB_WRITER"
},
"author": {
"id": 1,
"firstname": "Urasawa"
}
}
],
"editors": [
{
"id": 1,
"publicationDate": "0101-01-01T00:00:00+00:09",
"collection": null,
"isbn": "",
"editor": {
"id": 1,
"name": "Panini Comics"
}
}
],
"serie": {
"id": 1,
"name": "20th Century boys (Deluxe)"
}
},
...
]
You can tell your API to return those nested entities. Look at Book and Serie entities. You will find extra annotation with @Groups and attributes. In Book, look at the main annotation, and at related property (i only show title property but more are aimed by @Groups, look at original code here: https://github.com/Rebolon/php-sf-flex-webpack-encore-vuejs/blob/master/src/Entity/Library/Book.php):
/**
* @ApiResource(
* ...
* attributes={
* "normalization_context"={
* "groups"={"book_detail"}
* }
* }
* )
*
* @ORM\Entity
*/
class Book implements LibraryInterface {
...
/**
* @Groups("book_detail")
*
* @ORM\Column(type="string", length=255, nullable=false)
*
* @Assert\NotBlank()
* @Assert\Length(max="255")
*
*/
private $title;
...
And now the Serie entity, wher you only need to add @Groups on the properties you want to be returned:
...
/**
* @Groups("book_detail")
*
* @ORM\Column(type="string", length=512, nullable=false)
*
* @Assert\NotBlank()
* @Assert\Length(max="512")
*/
private $name;
...
Have a look at Kevin Dunglas slides because it gimme some clue for this feature: https://speakerdeck.com/dunglas/rest-vs-graphql-illustrated-examples-with-the-api-platform-framework His talk may be online in future but i don't kno when ;-) so here is the page that may link to the video, on day: https://github.com/SymfonyLive/paris-2018-talks
I still need to work on this feature because it would be cool to be able to build different route that may return IRIS or Normalized data. I don't know if it's possible and maybe i should go to GraphQL API because that's its job to do that kind of thing with its "Query".
There is a sample of custom route based on Action Demand Responder pattern that will allow to create new Books and it's dependancies in one HTTP call. You won't need to create sub-entity before creating the main one, said the book. The endpoint is /api/booksiu/special_3 [POST]. It takes the following JSON string as Body:
// The most complete sample, with de-duplication of editor (only once will be created)
{
"book": {
"title": "Zombies in western culture",
"editors": [{
"publication_date": "1519664915",
"collection": "printed version",
"isbn": "9781783743230",
"editor": {
"name": "Open Book Publishers"
}
}, {
"publication_date": "1519747464",
"collection": "ebooks",
"isbn": "9791036500824",
"editor": {
"name": "Open Book Publishers"
}
}],
"authors": [{
"role": {
"translation_key": "WRITER"
},
"author": {
"firstname": "Marc",
"lastname": "O'Brien"
}
}, {
"role": {
"translation_key": "WRITER"
},
"author": {
"firstname": "Paul",
"lastname": "Kyprianou"
}
}],
"serie": {
"name": "Open Reports Series"
}
}
}
// This one re-use database information for editor / author / job / serie
{
"book": {
"title": "Oh my god, how simple it is !",
"editors": [{
"publication_date": "1519664915",
"collection": "from my head",
"isbn": "9781783742530",
"editor": 1
}, {
"publication_date": "1519747464",
"collection": "ebooks",
"isbn": "9782821883963",
"editor": {
"name": "Open Book Publishers"
}
}],
"authors": [{
"role": 2,
"author": 3
}, {
"role": {
"translation_key": "WRITER"
},
"author": {
"firstname": "Paul",
"lastname": "Kyprianou"
}
}],
"serie": 4
}
}
pre-commit
hook that lint PHP and JS code, and run the PHP / JS testsquery getBooksAndSerieQry($firstBook: Int, $afterBook: String, $firstSerie: Int, $afterSerie: String) {
getBooksAndSerie: books(first: $firstBook, after: $afterBook) {
edges {
node {
id
title
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
getBooksAndSerie: series(first:$firstSerie, after: $afterSerie) {
edges {
node {
name
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
I wrote some articles on medium to explain some practices setup in this project: