├── .env ├── .env.test ├── .gitignore ├── Procfile ├── assets ├── css │ └── app.css └── js │ ├── app.js │ ├── components │ ├── Navbar.jsx │ ├── Pagination.jsx │ ├── PrivateRoute.jsx │ ├── forms │ │ ├── Field.jsx │ │ └── Select.jsx │ └── loaders │ │ ├── FormContentLoader.jsx │ │ └── TableLoader.jsx │ ├── config.js │ ├── contexts │ └── AuthContext.js │ ├── pages │ ├── CustomerPage.jsx │ ├── CustomersPage.jsx │ ├── CustomersPageWithPagination.jsx │ ├── HomePage.jsx │ ├── InvoicePage.jsx │ ├── InvoicesPage.jsx │ ├── LoginPage.jsx │ └── RegisterPage.jsx │ └── services │ ├── authAPI.js │ ├── cache.js │ ├── customersAPI.js │ ├── invoicesAPI.js │ └── usersAPI.js ├── bin ├── console └── phpunit ├── composer.json ├── composer.lock ├── config ├── bootstrap.php ├── bundles.php ├── jwt │ ├── private.pem │ └── public.pem ├── packages │ ├── api_platform.yaml │ ├── assets.yaml │ ├── cache.yaml │ ├── dev │ │ ├── debug.yaml │ │ ├── easy_log_handler.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── lexik_jwt_authentication.yaml │ ├── nelmio_cors.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ ├── monolog.yaml │ │ └── webpack_encore.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── sensio_framework_extra.yaml │ ├── swiftmailer.yaml │ ├── test │ │ ├── framework.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ ├── swiftmailer.yaml │ │ ├── validator.yaml │ │ └── web_profiler.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── validator.yaml │ └── webpack_encore.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ ├── api_platform.yaml │ └── dev │ │ ├── twig.yaml │ │ └── web_profiler.yaml └── services.yaml ├── package-lock.json ├── package.json ├── phpunit.xml.dist ├── public ├── .htaccess └── index.php ├── src ├── Controller │ ├── .gitignore │ ├── AppController.php │ └── InvoiceIncrementationController.php ├── DataFixtures │ └── AppFixtures.php ├── Doctrine │ └── CurrentUserExtension.php ├── Entity │ ├── .gitignore │ ├── Customer.php │ ├── Invoice.php │ └── User.php ├── Events │ ├── CustomerUserSubscriber.php │ ├── InvoiceChronoSubscriber.php │ ├── JwtCreatedSubscriber.php │ └── PasswordEncoderSubscriber.php ├── Kernel.php ├── Migrations │ ├── .gitignore │ ├── Version20190424130800.php │ ├── Version20190424131425.php │ ├── Version20190424132935.php │ └── Version20190424133911.php ├── Repository │ ├── .gitignore │ ├── CustomerRepository.php │ ├── InvoiceRepository.php │ └── UserRepository.php └── Serializer │ └── PatchedDateTimeNormalizer.php ├── symfony.lock ├── templates ├── app │ └── index.html.twig └── base.html.twig ├── test.js ├── tests └── .gitignore ├── translations └── .gitignore └── webpack.config.js /.env: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the later taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # 13 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 14 | # https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 15 | 16 | ###> symfony/framework-bundle ### 17 | APP_ENV=dev 18 | APP_SECRET=64fcaeb1cb558e388944955abc9f0306 19 | #TRUSTED_PROXIES=127.0.0.1,127.0.0.2 20 | #TRUSTED_HOSTS='^localhost|example\.com$' 21 | ###< symfony/framework-bundle ### 22 | 23 | ###> doctrine/doctrine-bundle ### 24 | # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 25 | # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" 26 | # Configure your db driver and server_version in config/packages/doctrine.yaml 27 | DATABASE_URL=mysql://root:@127.0.0.1:3306/symreact 28 | ###< doctrine/doctrine-bundle ### 29 | 30 | ###> symfony/swiftmailer-bundle ### 31 | # For Gmail as a transport, use: "gmail://username:password@localhost" 32 | # For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" 33 | # Delivery is disabled by default via "null://localhost" 34 | MAILER_URL=null://localhost 35 | ###< symfony/swiftmailer-bundle ### 36 | 37 | ###> nelmio/cors-bundle ### 38 | CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$ 39 | ###< nelmio/cors-bundle ### 40 | 41 | ###> Url de l'API pour notre application React 42 | API_URL="'http://localhost:8000/api/'" 43 | ###< Url de l'API 44 | 45 | ###> lexik/jwt-authentication-bundle ### 46 | JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem 47 | JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem 48 | JWT_PASSPHRASE=f31b041b721079c24826f6085db27e5d 49 | ###< lexik/jwt-authentication-bundle ### 50 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='s$cretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /public/bundles/ 7 | /var/ 8 | /vendor/ 9 | ###< symfony/framework-bundle ### 10 | 11 | ###> symfony/phpunit-bridge ### 12 | .phpunit 13 | /phpunit.xml 14 | ###< symfony/phpunit-bridge ### 15 | 16 | ###> symfony/web-server-bundle ### 17 | /.web-server-pid 18 | ###< symfony/web-server-bundle ### 19 | 20 | ###> lexik/jwt-authentication-bundle ### 21 | # /config/jwt/*.pem 22 | ###< lexik/jwt-authentication-bundle ### 23 | 24 | ###> symfony/webpack-encore-bundle ### 25 | /node_modules/ 26 | /public/build/ 27 | npm-debug.log 28 | yarn-error.log 29 | ###< symfony/webpack-encore-bundle ### 30 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: $(composer config bin-dir)/heroku-php-apache2 public/ -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liorchamla/formation-api-platform-react/599aeff7496c6c3a573501a36c961b1f05a62338/assets/css/app.css -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // Les imports importants 2 | import React, { useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { HashRouter, Route, Switch, withRouter } from "react-router-dom"; 5 | import Navbar from "./components/Navbar"; 6 | import PrivateRoute from "./components/PrivateRoute"; 7 | import AuthContext from "./contexts/AuthContext"; 8 | import CustomersPage from "./pages/CustomersPage"; 9 | import HomePage from "./pages/HomePage"; 10 | import InvoicesPage from "./pages/InvoicesPage"; 11 | import LoginPage from "./pages/LoginPage"; 12 | import AuthAPI from "./services/authAPI"; 13 | import CustomerPage from "./pages/CustomerPage"; 14 | import InvoicePage from "./pages/InvoicePage"; 15 | import RegisterPage from "./pages/RegisterPage"; 16 | import { ToastContainer, toast } from "react-toastify"; 17 | import "react-toastify/dist/ReactToastify.css"; 18 | 19 | // On apporte le CSS personnalisé 20 | require("../css/app.css"); 21 | 22 | AuthAPI.setup(); 23 | 24 | const App = () => { 25 | const [isAuthenticated, setIsAuthenticated] = useState( 26 | AuthAPI.isAuthenticated() 27 | ); 28 | 29 | const NavbarWithRouter = withRouter(Navbar); 30 | 31 | return ( 32 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 | 54 |
55 | ); 56 | }; 57 | 58 | const rootElement = document.querySelector("#app"); 59 | ReactDOM.render(, rootElement); 60 | -------------------------------------------------------------------------------- /assets/js/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import AuthAPI from "../services/authAPI"; 3 | import { NavLink } from "react-router-dom"; 4 | import AuthContext from "../contexts/AuthContext"; 5 | import { toast } from "react-toastify"; 6 | 7 | const Navbar = ({ history }) => { 8 | const { isAuthenticated, setIsAuthenticated } = useContext(AuthContext); 9 | 10 | const handleLogout = () => { 11 | AuthAPI.logout(); 12 | setIsAuthenticated(false); 13 | toast.info("Vous êtes désormais déconnecté 😁"); 14 | history.push("/login"); 15 | }; 16 | 17 | return ( 18 | 71 | ); 72 | }; 73 | 74 | export default Navbar; 75 | -------------------------------------------------------------------------------- /assets/js/components/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // 4 | const Pagination = ({ currentPage, itemsPerPage, length, onPageChanged }) => { 5 | const pagesCount = Math.ceil(length / itemsPerPage); 6 | const pages = []; 7 | 8 | for (let i = 1; i <= pagesCount; i++) { 9 | pages.push(i); 10 | } 11 | 12 | return ( 13 |
14 |
    15 |
  • 16 | 22 |
  • 23 | {pages.map(page => ( 24 |
  • 28 | 31 |
  • 32 | ))} 33 | 34 |
  • 37 | 43 |
  • 44 |
45 |
46 | ); 47 | }; 48 | 49 | Pagination.getData = (items, currentPage, itemsPerPage) => { 50 | const start = currentPage * itemsPerPage - itemsPerPage; 51 | return items.slice(start, start + itemsPerPage); 52 | }; 53 | 54 | export default Pagination; 55 | -------------------------------------------------------------------------------- /assets/js/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Redirect, Route } from "react-router-dom"; 3 | import AuthContext from "../contexts/AuthContext"; 4 | 5 | const PrivateRoute = ({ path, component }) => { 6 | const { isAuthenticated } = useContext(AuthContext); 7 | 8 | return isAuthenticated ? ( 9 | 10 | ) : ( 11 | 12 | ); 13 | }; 14 | 15 | export default PrivateRoute; 16 | -------------------------------------------------------------------------------- /assets/js/components/forms/Field.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Field = ({ 4 | name, 5 | label, 6 | value, 7 | onChange, 8 | placeholder = "", 9 | type = "text", 10 | error = "" 11 | }) => ( 12 |
13 | 14 | 23 | {error &&

{error}

} 24 |
25 | ); 26 | 27 | export default Field; 28 | -------------------------------------------------------------------------------- /assets/js/components/forms/Select.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Select = ({ name, value, error = "", label, onChange, children }) => { 4 | return ( 5 |
6 | 7 | 16 |

{error}

17 |
18 | ); 19 | }; 20 | 21 | export default Select; 22 | -------------------------------------------------------------------------------- /assets/js/components/loaders/FormContentLoader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ContentLoader from "react-content-loader"; 3 | 4 | const FormContentLoader = props => ( 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | export default FormContentLoader; 26 | -------------------------------------------------------------------------------- /assets/js/components/loaders/TableLoader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ContentLoader from "react-content-loader"; 3 | 4 | const TableRow = props => { 5 | const random = Math.random() * (1 - 0.7) + 0.7; 6 | return ( 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | const TableLoader = () => ( 28 | 29 | {Array(10) 30 | .fill("") 31 | .map((e, i) => ( 32 | 33 | ))} 34 | 35 | ); 36 | 37 | export default TableLoader; 38 | -------------------------------------------------------------------------------- /assets/js/config.js: -------------------------------------------------------------------------------- 1 | export const API_URL = process.env.API_URL; 2 | 3 | export const CUSTOMERS_API = API_URL + "customers"; 4 | export const INVOICES_API = API_URL + "invoices"; 5 | export const USERS_API = API_URL + "users"; 6 | export const LOGIN_API = API_URL + "login_check"; 7 | -------------------------------------------------------------------------------- /assets/js/contexts/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default React.createContext({ 4 | isAuthenticated: false, 5 | setIsAuthenticated: value => {} 6 | }); 7 | -------------------------------------------------------------------------------- /assets/js/pages/CustomerPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Field from "./../components/forms/Field"; 3 | import { Link } from "react-router-dom"; 4 | import CustomersAPI from "../services/customersAPI"; 5 | import { toast } from "react-toastify"; 6 | import FormContentLoader from "../components/loaders/FormContentLoader"; 7 | 8 | const CustomerPage = ({ match, history }) => { 9 | const { id = "new" } = match.params; 10 | 11 | const [customer, setCustomer] = useState({ 12 | lastName: "", 13 | firstName: "", 14 | email: "", 15 | company: "" 16 | }); 17 | const [errors, setErrors] = useState({ 18 | lastName: "", 19 | firstName: "", 20 | email: "", 21 | company: "" 22 | }); 23 | const [loading, setLoading] = useState(false); 24 | const [editing, setEditing] = useState(false); 25 | 26 | // Récupération du customer en fonction de l'identifiant 27 | const fetchCustomer = async id => { 28 | try { 29 | const { firstName, lastName, email, company } = await CustomersAPI.find( 30 | id 31 | ); 32 | setCustomer({ firstName, lastName, email, company }); 33 | setLoading(false); 34 | } catch (error) { 35 | toast.error("Le client n'a pas pu être chargé"); 36 | history.replace("/customers"); 37 | } 38 | }; 39 | 40 | // Chargement du customer si besoin au chargement du composant ou au changement de l'identifiant 41 | useEffect(() => { 42 | if (id !== "new") { 43 | setLoading(true); 44 | setEditing(true); 45 | fetchCustomer(id); 46 | } 47 | }, [id]); 48 | 49 | // Gestion des changements des inputs dans le formulaire 50 | const handleChange = ({ currentTarget }) => { 51 | const { name, value } = currentTarget; 52 | setCustomer({ ...customer, [name]: value }); 53 | }; 54 | 55 | // Gestion de la soumission du formulaire 56 | const handleSubmit = async event => { 57 | event.preventDefault(); 58 | 59 | try { 60 | setErrors({}); 61 | 62 | if (editing) { 63 | await CustomersAPI.update(id, customer); 64 | toast.success("Le client a bien été modifié"); 65 | } else { 66 | await CustomersAPI.create(customer); 67 | toast.success("Le client a bien été créé"); 68 | history.replace("/customers"); 69 | } 70 | } catch ({ response }) { 71 | const { violations } = response.data; 72 | 73 | if (violations) { 74 | const apiErrors = {}; 75 | violations.forEach(({ propertyPath, message }) => { 76 | apiErrors[propertyPath] = message; 77 | }); 78 | 79 | setErrors(apiErrors); 80 | toast.error("Des erreurs dans votre formulaire !"); 81 | } 82 | } 83 | }; 84 | 85 | return ( 86 | <> 87 | {(!editing &&

Création d'un client

) || ( 88 |

Modification du client

89 | )} 90 | 91 | {loading && } 92 | {!loading && ( 93 |
94 | 102 | 110 | 119 | 127 | 128 |
129 | 132 | 133 | Retour à la liste 134 | 135 |
136 | 137 | )} 138 | 139 | ); 140 | }; 141 | 142 | export default CustomerPage; 143 | -------------------------------------------------------------------------------- /assets/js/pages/CustomersPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Pagination from "../components/Pagination"; 3 | import CustomersAPI from "../services/customersAPI"; 4 | import { Link } from "react-router-dom"; 5 | import { toast } from "react-toastify"; 6 | import TableLoader from "../components/loaders/TableLoader"; 7 | 8 | const CustomersPage = props => { 9 | const [customers, setCustomers] = useState([]); 10 | const [currentPage, setCurrentPage] = useState(1); 11 | const [search, setSearch] = useState(""); 12 | const [loading, setLoading] = useState(true); 13 | 14 | // Permet d'aller récupérer les customers 15 | const fetchCustomers = async () => { 16 | try { 17 | const data = await CustomersAPI.findAll(); 18 | setCustomers(data); 19 | setLoading(false); 20 | } catch (error) { 21 | toast.error("Impossible de charger les clients"); 22 | } 23 | }; 24 | 25 | // Au chargement du composant, on va chercher les customers 26 | useEffect(() => { 27 | fetchCustomers(); 28 | }, []); 29 | 30 | // Gestion de la suppression d'un customer 31 | const handleDelete = async id => { 32 | const originalCustomers = [...customers]; 33 | setCustomers(customers.filter(customer => customer.id !== id)); 34 | 35 | try { 36 | await CustomersAPI.delete(id); 37 | toast.success("Le client a bien été supprimé"); 38 | } catch (error) { 39 | setCustomers(originalCustomers); 40 | toast.error("La suppression du client n'a pas pu fonctionner"); 41 | } 42 | }; 43 | 44 | // Gestion du changement de page 45 | const handlePageChange = page => setCurrentPage(page); 46 | 47 | // Gestion de la recherche 48 | const handleSearch = ({ currentTarget }) => { 49 | setSearch(currentTarget.value); 50 | setCurrentPage(1); 51 | }; 52 | 53 | const itemsPerPage = 10; 54 | 55 | // Filtrage des customers en fonction de la recherche 56 | const filteredCustomers = customers.filter( 57 | c => 58 | c.firstName.toLowerCase().includes(search.toLowerCase()) || 59 | c.lastName.toLowerCase().includes(search.toLowerCase()) || 60 | c.email.toLowerCase().includes(search.toLowerCase()) || 61 | (c.company && c.company.toLowerCase().includes(search.toLowerCase())) 62 | ); 63 | 64 | // Pagination des données 65 | const paginatedCustomers = Pagination.getData( 66 | filteredCustomers, 67 | currentPage, 68 | itemsPerPage 69 | ); 70 | 71 | return ( 72 | <> 73 |
74 |

Liste des clients

75 | 76 | Créer un client 77 | 78 |
79 | 80 |
81 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 101 | 102 | 103 | {!loading && ( 104 | 105 | {paginatedCustomers.map(customer => ( 106 | 107 | 108 | 113 | 114 | 115 | 120 | 123 | 132 | 133 | ))} 134 | 135 | )} 136 |
Id.ClientEmailEntrepriseFacturesMontant total 100 |
{customer.id} 109 | 110 | {customer.firstName} {customer.lastName} 111 | 112 | {customer.email}{customer.company} 116 | 117 | {customer.invoices.length} 118 | 119 | 121 | {customer.totalAmount.toLocaleString()} € 122 | 124 | 131 |
137 | 138 | {loading && } 139 | 140 | {itemsPerPage < filteredCustomers.length && ( 141 | 147 | )} 148 | 149 | ); 150 | }; 151 | 152 | export default CustomersPage; 153 | -------------------------------------------------------------------------------- /assets/js/pages/CustomersPageWithPagination.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | import Pagination from "../components/Pagination"; 4 | 5 | const CustomersPageWithPagination = props => { 6 | const [customers, setCustomers] = useState([]); 7 | const [totalItems, setTotalItems] = useState(0); 8 | const [currentPage, setCurrentPage] = useState(1); 9 | const [loading, setLoading] = useState(true); 10 | const itemsPerPage = 10; 11 | 12 | useEffect(() => { 13 | axios 14 | .get( 15 | `http://localhost:8000/api/customers?pagination=true&count=${itemsPerPage}&page=${currentPage}` 16 | ) 17 | .then(response => { 18 | setCustomers(response.data["hydra:member"]); 19 | setTotalItems(response.data["hydra:totalItems"]); 20 | setLoading(false); 21 | }) 22 | .catch(error => console.log(error.response)); 23 | }, [currentPage]); 24 | 25 | const handleDelete = id => { 26 | const originalCustomers = [...customers]; 27 | 28 | // 1. L'approche optimiste 29 | setCustomers(customers.filter(customer => customer.id !== id)); 30 | 31 | // 2. L'approche pessimiste 32 | axios 33 | .delete("http://localhost:8000/api/customers/" + id) 34 | .then(response => console.log("ok")) 35 | .catch(error => { 36 | setCustomers(originalCustomers); 37 | console.log(error.response); 38 | }); 39 | }; 40 | 41 | const handlePageChange = page => { 42 | setCurrentPage(page); 43 | setLoading(true); 44 | }; 45 | 46 | const paginatedCustomers = Pagination.getData( 47 | customers, 48 | currentPage, 49 | itemsPerPage 50 | ); 51 | 52 | return ( 53 | <> 54 |

Liste des clients (pagination)

55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 67 | 68 | 69 | 70 | {loading && ( 71 | 72 | 73 | 74 | )} 75 | {!loading && 76 | customers.map(customer => ( 77 | 78 | 79 | 84 | 85 | 86 | 91 | 94 | 103 | 104 | ))} 105 | 106 |
Id.ClientEmailEntrepriseFacturesMontant total 66 |
Chargement ...
{customer.id} 80 | 81 | {customer.firstName} {customer.lastName} 82 | 83 | {customer.email}{customer.company} 87 | 88 | {customer.invoices.length} 89 | 90 | 92 | {customer.totalAmount.toLocaleString()} € 93 | 95 | 102 |
107 | 108 | 114 | 115 | ); 116 | }; 117 | 118 | export default CustomersPageWithPagination; 119 | -------------------------------------------------------------------------------- /assets/js/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const HomePage = props => { 4 | return ( 5 |
6 |

Hello, world!

7 |

8 | This is a simple hero unit, a simple jumbotron-style component for 9 | calling extra attention to featured content or information. 10 |

11 |
12 |

13 | It uses utility classes for typography and spacing to space content out 14 | within the larger container. 15 |

16 |

17 | 18 | Learn more 19 | 20 |

21 |
22 | ); 23 | }; 24 | 25 | export default HomePage; 26 | -------------------------------------------------------------------------------- /assets/js/pages/InvoicePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Field from "../components/forms/Field"; 3 | import Select from "../components/forms/Select"; 4 | import { Link } from "react-router-dom"; 5 | import CustomersAPI from "../services/customersAPI"; 6 | import InvoicesAPI from "../services/invoicesAPI"; 7 | import { toast } from "react-toastify"; 8 | import FormContentLoader from "../components/loaders/FormContentLoader"; 9 | 10 | const InvoicePage = ({ history, match }) => { 11 | const { id = "new" } = match.params; 12 | 13 | const [invoice, setInvoice] = useState({ 14 | amount: "", 15 | customer: "", 16 | status: "SENT" 17 | }); 18 | const [customers, setCustomers] = useState([]); 19 | const [editing, setEditing] = useState(false); 20 | const [errors, setErrors] = useState({ 21 | amount: "", 22 | customer: "", 23 | status: "" 24 | }); 25 | const [loading, setLoading] = useState(true); 26 | 27 | // Récupération des clients 28 | const fetchCustomers = async () => { 29 | try { 30 | const data = await CustomersAPI.findAll(); 31 | setCustomers(data); 32 | setLoading(false); 33 | 34 | if (!invoice.customer) setInvoice({ ...invoice, customer: data[0].id }); 35 | } catch (error) { 36 | toast.error("Impossible de charger les clients"); 37 | history.replace("/invoices"); 38 | } 39 | }; 40 | 41 | // Récupération d'une facture 42 | const fetchInvoice = async id => { 43 | try { 44 | const { amount, status, customer } = await InvoicesAPI.find(id); 45 | setInvoice({ amount, status, customer: customer.id }); 46 | setLoading(false); 47 | } catch (error) { 48 | toast.error("Impossible de charger la facture demandée"); 49 | history.replace("/invoices"); 50 | } 51 | }; 52 | 53 | // Récupération de la liste des clients à chaque chargement du composant 54 | useEffect(() => { 55 | fetchCustomers(); 56 | }, []); 57 | 58 | // Récupération de la bonne facture quand l'identifiant de l'URL change 59 | useEffect(() => { 60 | if (id !== "new") { 61 | setEditing(true); 62 | fetchInvoice(id); 63 | } 64 | }, [id]); 65 | 66 | // Gestion des changements des inputs dans le formulaire 67 | const handleChange = ({ currentTarget }) => { 68 | const { name, value } = currentTarget; 69 | setInvoice({ ...invoice, [name]: value }); 70 | }; 71 | 72 | // Gestion de la soumission du formulaire 73 | const handleSubmit = async event => { 74 | event.preventDefault(); 75 | 76 | try { 77 | if (editing) { 78 | await InvoicesAPI.update(id, invoice); 79 | toast.success("La facture a bien été modifiée"); 80 | } else { 81 | await InvoicesAPI.create(invoice); 82 | toast.success("La facture a bien été enregistrée"); 83 | history.replace("/invoices"); 84 | } 85 | } catch ({ response }) { 86 | const { violations } = response.data; 87 | 88 | if (violations) { 89 | const apiErrors = {}; 90 | violations.forEach(({ propertyPath, message }) => { 91 | apiErrors[propertyPath] = message; 92 | }); 93 | 94 | setErrors(apiErrors); 95 | toast.error("Des erreurs dans votre formulaire"); 96 | } 97 | } 98 | }; 99 | 100 | return ( 101 | <> 102 | {(editing &&

Modification d'une facture

) || ( 103 |

Création d'une facture

104 | )} 105 | {loading && } 106 | 107 | {!loading && ( 108 |
109 | 118 | 119 | 132 | 133 | 144 | 145 |
146 | 149 | 150 | Retour aux factures 151 | 152 |
153 | 154 | )} 155 | 156 | ); 157 | }; 158 | 159 | export default InvoicePage; 160 | -------------------------------------------------------------------------------- /assets/js/pages/InvoicesPage.jsx: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import React, { useEffect, useState } from "react"; 3 | import Pagination from "../components/Pagination"; 4 | import InvoicesAPI from "../services/invoicesAPI"; 5 | import { Link } from "react-router-dom"; 6 | import { toast } from "react-toastify"; 7 | import TableLoader from "../components/loaders/TableLoader"; 8 | 9 | const STATUS_CLASSES = { 10 | PAID: "success", 11 | SENT: "primary", 12 | CANCELLED: "danger" 13 | }; 14 | 15 | const STATUS_LABELS = { 16 | PAID: "Payée", 17 | SENT: "Envoyée", 18 | CANCELLED: "Annulée" 19 | }; 20 | 21 | const InvoicesPage = props => { 22 | const [invoices, setInvoices] = useState([]); 23 | const [currentPage, setCurrentPage] = useState(1); 24 | const [search, setSearch] = useState(""); 25 | const [loading, setLoading] = useState(true); 26 | const itemsPerPage = 10; 27 | 28 | // Récupération des invoices auprès de l'API 29 | const fetchInvoices = async () => { 30 | try { 31 | const data = await InvoicesAPI.findAll(); 32 | setInvoices(data); 33 | setLoading(false); 34 | } catch (error) { 35 | toast.error("Erreur lors du chargement des factures !"); 36 | } 37 | }; 38 | 39 | // Charger les invoices au chargement du composant 40 | useEffect(() => { 41 | fetchInvoices(); 42 | }, []); 43 | 44 | // Gestion du changement de page 45 | const handlePageChange = page => setCurrentPage(page); 46 | 47 | // Gestion de la recherche 48 | const handleSearch = ({ currentTarget }) => { 49 | setSearch(currentTarget.value); 50 | setCurrentPage(1); 51 | }; 52 | 53 | // Gestion de la suppression 54 | const handleDelete = async id => { 55 | const originalInvoices = [...invoices]; 56 | 57 | setInvoices(invoices.filter(invoice => invoice.id !== id)); 58 | 59 | try { 60 | await InvoicesAPI.delete(id); 61 | toast.success("La facture a bien été supprimée"); 62 | } catch (error) { 63 | toast.error("Une erreur est survenue"); 64 | setInvoices(originalInvoices); 65 | } 66 | }; 67 | 68 | // Gestion du format de date 69 | const formatDate = str => moment(str).format("DD/MM/YYYY"); 70 | 71 | // Gestion de la recherche : 72 | const filteredInvoices = invoices.filter( 73 | i => 74 | i.customer.firstName.toLowerCase().includes(search.toLowerCase()) || 75 | i.customer.lastName.toLowerCase().includes(search.toLowerCase()) || 76 | i.amount.toString().startsWith(search.toLowerCase()) || 77 | STATUS_LABELS[i.status].toLowerCase().includes(search.toLowerCase()) 78 | ); 79 | 80 | // Pagination des données 81 | const paginatedInvoices = Pagination.getData( 82 | filteredInvoices, 83 | currentPage, 84 | itemsPerPage 85 | ); 86 | 87 | return ( 88 | <> 89 |
90 |

Liste des factures

91 | 92 | Créer une facture 93 | 94 |
95 | 96 |
97 | 104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 116 | 117 | {!loading && ( 118 | 119 | {paginatedInvoices.map(invoice => ( 120 | 121 | 122 | 127 | 128 | 135 | 138 | 152 | 153 | ))} 154 | 155 | )} 156 |
NuméroClientDate d'envoiStatutMontant 115 |
{invoice.chrono} 123 | 124 | {invoice.customer.firstName} {invoice.customer.lastName} 125 | 126 | {formatDate(invoice.sentAt)} 129 | 132 | {STATUS_LABELS[invoice.status]} 133 | 134 | 136 | {invoice.amount.toLocaleString()} € 137 | 139 | 143 | Editer 144 | 145 | 151 |
157 | 158 | {loading && } 159 | 160 | 166 | 167 | ); 168 | }; 169 | 170 | export default InvoicesPage; 171 | -------------------------------------------------------------------------------- /assets/js/pages/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import AuthAPI from "../services/authAPI"; 3 | import AuthContext from "../contexts/AuthContext"; 4 | import Field from "../components/forms/Field"; 5 | import { toast } from "react-toastify"; 6 | 7 | const LoginPage = ({ history }) => { 8 | const { setIsAuthenticated } = useContext(AuthContext); 9 | 10 | const [credentials, setCredentials] = useState({ 11 | username: "", 12 | password: "" 13 | }); 14 | const [error, setError] = useState(""); 15 | 16 | // Gestion des champs 17 | const handleChange = ({ currentTarget }) => { 18 | const { value, name } = currentTarget; 19 | setCredentials({ ...credentials, [name]: value }); 20 | }; 21 | 22 | // Gestion du submit 23 | const handleSubmit = async event => { 24 | event.preventDefault(); 25 | 26 | try { 27 | await AuthAPI.authenticate(credentials); 28 | setError(""); 29 | setIsAuthenticated(true); 30 | toast.success("Vous êtes désormais connecté !"); 31 | history.replace("/customers"); 32 | } catch (error) { 33 | setError( 34 | "Aucun compte ne possède cette adresse email ou alors les informations ne correspondent pas !" 35 | ); 36 | toast.error("Une erreur est survenue"); 37 | } 38 | }; 39 | 40 | return ( 41 | <> 42 |

Connexion à l'application

43 | 44 |
45 | 53 | 54 | 62 | 63 |
64 | 67 |
68 | 69 | 70 | ); 71 | }; 72 | 73 | export default LoginPage; 74 | -------------------------------------------------------------------------------- /assets/js/pages/RegisterPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Field from "./../components/forms/Field"; 3 | import { Link } from "react-router-dom"; 4 | import UsersAPI from "../services/usersAPI"; 5 | import { toast } from "react-toastify"; 6 | 7 | const RegisterPage = ({ history }) => { 8 | const [user, setUser] = useState({ 9 | firstName: "", 10 | lastName: "", 11 | email: "", 12 | password: "", 13 | passwordConfirm: "" 14 | }); 15 | 16 | const [errors, setErrors] = useState({ 17 | firstName: "", 18 | lastName: "", 19 | email: "", 20 | password: "", 21 | passwordConfirm: "" 22 | }); 23 | 24 | // Gestion des changements des inputs dans le formulaire 25 | const handleChange = ({ currentTarget }) => { 26 | const { name, value } = currentTarget; 27 | setUser({ ...user, [name]: value }); 28 | }; 29 | 30 | // Gestion de la soumission 31 | const handleSubmit = async event => { 32 | event.preventDefault(); 33 | 34 | const apiErrors = {}; 35 | 36 | if (user.password !== user.passwordConfirm) { 37 | apiErrors.passwordConfirm = 38 | "Votre confirmation de mot de passe n'est pas conforme avec le mot de passe original"; 39 | setErrors(apiErrors); 40 | toast.error("Des erreurs dans votre formulaire !"); 41 | return; 42 | } 43 | 44 | try { 45 | await UsersAPI.register(user); 46 | setErrors({}); 47 | 48 | // TODO : Flash success 49 | toast.success( 50 | "Vous êtes désormais inscrit, vous pouvez vous connecter !" 51 | ); 52 | history.replace("/login"); 53 | } catch (error) { 54 | const { violations } = error.response.data; 55 | 56 | if (violations) { 57 | violations.forEach(violation => { 58 | apiErrors[violation.propertyPath] = violation.message; 59 | }); 60 | setErrors(apiErrors); 61 | } 62 | toast.error("Des erreurs dans votre formulaire !"); 63 | } 64 | }; 65 | 66 | return ( 67 | <> 68 |

Inscription

69 |
70 | 78 | 86 | 95 | 104 | 113 | 114 |
115 | 118 | 119 | J'ai déjà un compte 120 | 121 |
122 | 123 | 124 | ); 125 | }; 126 | 127 | export default RegisterPage; 128 | -------------------------------------------------------------------------------- /assets/js/services/authAPI.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import jwtDecode from "jwt-decode"; 3 | import { LOGIN_API } from "../config"; 4 | 5 | /** 6 | * Déconnexion (suppression du token du localStorage et sur Axios) 7 | */ 8 | function logout() { 9 | window.localStorage.removeItem("authToken"); 10 | delete axios.defaults.headers["Authorization"]; 11 | } 12 | 13 | /** 14 | * Requête HTTP d'authentification et stockage du token dans le storage et sur Axios 15 | * @param {object} credentials 16 | */ 17 | function authenticate(credentials) { 18 | return axios 19 | .post(LOGIN_API, credentials) 20 | .then(response => response.data.token) 21 | .then(token => { 22 | // Je stocke le token dans mon localStorage 23 | window.localStorage.setItem("authToken", token); 24 | // On prévient Axios qu'on a maintenant un header par défaut sur toutes nos futures requetes HTTP 25 | setAxiosToken(token); 26 | }); 27 | } 28 | 29 | /** 30 | * Positionne le token JWT sur Axios 31 | * @param {string} token Le token JWT 32 | */ 33 | function setAxiosToken(token) { 34 | axios.defaults.headers["Authorization"] = "Bearer " + token; 35 | } 36 | 37 | /** 38 | * Mise en place lors du chargement de l'application 39 | */ 40 | function setup() { 41 | // 1. Voir si on a un token ? 42 | const token = window.localStorage.getItem("authToken"); 43 | // 2. Si le token est encore valide 44 | if (token) { 45 | const { exp: expiration } = jwtDecode(token); 46 | if (expiration * 1000 > new Date().getTime()) { 47 | setAxiosToken(token); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Permet de savoir si on est authentifié ou pas 54 | * @returns boolean 55 | */ 56 | function isAuthenticated() { 57 | // 1. Voir si on a un token ? 58 | const token = window.localStorage.getItem("authToken"); 59 | // 2. Si le token est encore valide 60 | if (token) { 61 | const { exp: expiration } = jwtDecode(token); 62 | if (expiration * 1000 > new Date().getTime()) { 63 | return true; 64 | } 65 | return false; 66 | } 67 | return false; 68 | } 69 | 70 | export default { 71 | authenticate, 72 | logout, 73 | setup, 74 | isAuthenticated 75 | }; 76 | -------------------------------------------------------------------------------- /assets/js/services/cache.js: -------------------------------------------------------------------------------- 1 | const cache = {}; 2 | 3 | function set(key, data) { 4 | cache[key] = { 5 | data, 6 | cachedAt: new Date().getTime() 7 | }; 8 | } 9 | 10 | function get(key) { 11 | return new Promise(resolve => { 12 | resolve( 13 | cache[key] && cache[key].cachedAt + 15 * 60 * 1000 > new Date().getTime() 14 | ? cache[key].data 15 | : null 16 | ); 17 | }); 18 | } 19 | 20 | function invalidate(key) { 21 | delete cache[key]; 22 | } 23 | 24 | export default { 25 | set, 26 | get, 27 | invalidate 28 | }; 29 | -------------------------------------------------------------------------------- /assets/js/services/customersAPI.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import Cache from "./cache"; 3 | import { CUSTOMERS_API } from "../config"; 4 | 5 | async function findAll() { 6 | const cachedCustomers = await Cache.get("customers"); 7 | 8 | if (cachedCustomers) return cachedCustomers; 9 | 10 | return axios.get(CUSTOMERS_API).then(response => { 11 | const customers = response.data["hydra:member"]; 12 | Cache.set("customers", customers); 13 | return customers; 14 | }); 15 | } 16 | 17 | async function find(id) { 18 | const cachedCustomer = await Cache.get("customers." + id); 19 | 20 | if (cachedCustomer) return cachedCustomer; 21 | 22 | return axios.get(CUSTOMERS_API + "/" + id).then(response => { 23 | const customer = response.data; 24 | 25 | Cache.set("customers." + id, customer); 26 | 27 | return customer; 28 | }); 29 | } 30 | 31 | function deleteCustomer(id) { 32 | return axios.delete(CUSTOMERS_API + "/" + id).then(async response => { 33 | const cachedCustomers = await Cache.get("customers"); 34 | 35 | if (cachedCustomers) { 36 | Cache.set("customers", cachedCustomers.filter(c => c.id !== id)); 37 | } 38 | 39 | return response; 40 | }); 41 | } 42 | 43 | function update(id, customer) { 44 | return axios.put(CUSTOMERS_API + "/" + id, customer).then(async response => { 45 | const cachedCustomers = await Cache.get("customers"); 46 | const cachedCustomer = await Cache.get("customers." + id); 47 | 48 | if (cachedCustomer) { 49 | Cache.set("customers." + id, response.data); 50 | } 51 | 52 | if (cachedCustomers) { 53 | const index = cachedCustomers.findIndex(c => c.id === +id); 54 | cachedCustomers[index] = response.data; 55 | } 56 | 57 | return response; 58 | }); 59 | } 60 | 61 | function create(customer) { 62 | return axios.post(CUSTOMERS_API, customer).then(async response => { 63 | const cachedCustomers = await Cache.get("customers"); 64 | 65 | if (cachedCustomers) { 66 | Cache.set("customers", [...cachedCustomers, response.data]); 67 | } 68 | 69 | return response; 70 | }); 71 | } 72 | 73 | export default { 74 | findAll, 75 | find, 76 | create, 77 | update, 78 | delete: deleteCustomer 79 | }; 80 | -------------------------------------------------------------------------------- /assets/js/services/invoicesAPI.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { INVOICES_API } from "../config"; 3 | 4 | function findAll() { 5 | return axios 6 | .get(INVOICES_API) 7 | .then(response => response.data["hydra:member"]); 8 | } 9 | 10 | function deleteInvoice(id) { 11 | return axios.delete(INVOICES_API + "/" + id); 12 | } 13 | 14 | function find(id) { 15 | return axios.get(INVOICES_API + "/" + id).then(response => response.data); 16 | } 17 | 18 | function update(id, invoice) { 19 | return axios.put(INVOICES_API + "/" + id, { 20 | ...invoice, 21 | customer: `/api/customers/${invoice.customer}` 22 | }); 23 | } 24 | 25 | function create(invoice) { 26 | return axios.post(INVOICES_API, { 27 | ...invoice, 28 | customer: `/api/customers/${invoice.customer}` 29 | }); 30 | } 31 | 32 | export default { 33 | findAll, 34 | find, 35 | create, 36 | update, 37 | delete: deleteInvoice 38 | }; 39 | -------------------------------------------------------------------------------- /assets/js/services/usersAPI.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { USERS_API } from "../config"; 3 | 4 | function register(user) { 5 | return axios.post(USERS_API, user); 6 | } 7 | 8 | export default { 9 | register 10 | }; 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 23 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 24 | } 25 | 26 | if ($input->hasParameterOption('--no-debug', true)) { 27 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 28 | } 29 | 30 | require dirname(__DIR__).'/config/bootstrap.php'; 31 | 32 | if ($_SERVER['APP_DEBUG']) { 33 | umask(0000); 34 | 35 | if (class_exists(Debug::class)) { 36 | Debug::enable(); 37 | } 38 | } 39 | 40 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 41 | $application = new Application($kernel); 42 | $application->run($input); 43 | -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =1.2) 9 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) { 10 | $_ENV += $env; 11 | } elseif (!class_exists(Dotenv::class)) { 12 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 13 | } else { 14 | // load all the .env files 15 | (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); 16 | } 17 | 18 | $_SERVER += $_ENV; 19 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 20 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 21 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 22 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], 6 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 7 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 8 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 9 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 10 | Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], 11 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 12 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 13 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 14 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], 15 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 16 | Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], 17 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], 18 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], 19 | ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], 20 | Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], 21 | Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], 22 | ]; 23 | -------------------------------------------------------------------------------- /config/jwt/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-256-CBC,40E874BA368EB07886DACB5F96A6E18C 4 | 5 | up/ZJKRiYpjr/uwcDedlqPDs2DdMxBsnT18ShJN7VsGQmKgVm9STjQkdcof1e5i1 6 | I5H8LxXMDGuvz6qV4oyhDqMfEKSx9Nmf+b8yCOFAqAtQR+WhboahKMXBhh14zHQo 7 | qvuXOxrOb45Nq4Y5DhXCJkkr/MYgjC2SegW/44sxJpoZxLqAiNMjEqcN///N5JOn 8 | VrSE8ylrd0leP0R7m15cs+K2kesD5nKcHVZHjUyPrlF77GrcVX9Z4EWmTHSt3XF6 9 | 0lXZ//Wauh7o4gLdUs0FinNT8fnNXFjcfp8r9I5LwWf+y+WyRYsvf8Ece6nFS8+i 10 | jMvD1FdM4eg/Te71mjftE1hdv2pEA9oe+LKh7iyB4gS9ACrH9G5m7Irx+VPLT2tx 11 | ppr9Xqgyqq6aw2Okdl8vsv1DdfwVGHWkyPnr66S2wejylKaFA/AxOVQM8u2Qxscy 12 | tnlv51Ad+ooLgfrMHT2WWSQfv8vMmIr6iqvpzQybuI4zAS9opdNQ+O+XoANxz++A 13 | u0FQ6lmhxl+Rotqeh6Aa3XCBD7Rhc+dumNGYFiz2ZvgrSHSW+mncwsJWId2tA2ep 14 | 3xXIpEeyDDpmI4UaxZC1sGIKcm6AdJ0PCjOKcWMttUNj3lmnBYLOqDXJ4Hk5IaWD 15 | RtPGqALVKfZbRfhjYvQNNkjiI3P/+k1HY9nkKIrirjZ/z0+tVkgrypXR6MR9edgH 16 | ONegnV1xTenOFYJ4JHBq8HkFM9bzXDN1wnZyLCBcHhU4D65Et1IkUQxH7KSo2Dkf 17 | GeRClRLruf8M+Pc7sAQXq2AzeoRXhcIlLJ5hxAjJMFtIGvV1dCdDxQRYiUg83uTj 18 | dR5bKEOvOsD6m6oY04/Cx5VGL1dxaxhZgxHyFmT32pIZGgmGv092kvpv/4yo+So1 19 | TDsucUJKIoi1wu/Z+cO9KSTefdVdBftlHXdLlohTouQz06BgV5Q0lBHiG9LP2AL/ 20 | DfgEmgieXU24HvxHXfTVt0H8bvqELRZM+n69xOYoXJ1dv03L1bT4eqbGgbmV016t 21 | kUl8/HVMzKQOqpEDbProROPz4jIX6G/7D3gkqhs6eobk4G1sD1H00C7MJCOfaf79 22 | rLcNrt1F4KwwjxjcdYPyRLSqHumeu5LZ5Lbmc5cCCD19bhEwCk/FCmXFYusk/2Ye 23 | OUJevrsrNATwml36n4RlqGxdUJwWoKRGURfw9kn/RNzbsoOKgi0ChQiI0urNsVk7 24 | YgYOtOTJFOXNsn/dkHMM2TeuLR1taZyR1Kz4DFDNll8KEQvABzeozz44gzg87IlE 25 | eZ1gd9RKJ25bxUtO8k3Zx8d6cBHuxnL3gzpwTklwX4WX54xcYUImIhNuKsquM58p 26 | D2fp7lnCIR7u32MDP5yPVkd/QMyPwxcpbrys7L6CthUhJxiSpQQuTsOn8LHoJqh6 27 | 5l0DbDaF0J6de1+uE/z286ygP2mTv/5mT5W64A0/qkg7adB/b1/6fSWXjmJsQDNk 28 | erCNbVFD8W5yVKOmyWYZ3UqmjVBxryKauIYQlyQM7RuR4r23YL5nKjcozYa+LtkF 29 | Uqk91Hpi8V0Z8wvb3vliKqTjP7AJGKaX/G1JbcZXiPVCI0MPtE/jFe2YU/VEcAbZ 30 | RIDjB9/bT3F4BeVAlt7GstOdVrcD0oEb1Kn6kOnylQD4V31+Ac6grHF0DqLwrUQf 31 | lBTeJ97VphgZ+iPnf/6j3CpiB0LQVIpkZ31MNf042x3Sg4AwhSM7OP55F7sk4qDe 32 | frE23l2KcPvpvPhO3VSxa3Fi3tDdJDGwrXl7wFWCQSnhgB8y1m/RvSflXZt8aDsQ 33 | SPFXOkI6QXtZFu1UxCEEkU5qIaojtcyJ+2Phudfw6xTuZ3ZKutcmho0X6mF+mxTe 34 | Ba/QQXnjJM5sU7aO3AgmxhZQtnX+/qnBf5P0cjSEkIogYpAjVbY3tucdjbOL2G2s 35 | tRHiQOg30JfAWEPRJN7ouGBo00G3uWqIN6W9H8X7GLbOAm+MlD1zflRJspnIuEDy 36 | GMa/MCoB3lVNewVYkdOLQM+xWF8UmTQRLGzzduUho4tD6auRxy3b8W3E8ElLc07e 37 | i1T9Bk5NpmJQBY7tQfyYykTCUJYjp8iLIqvn1dfU9Lro2jEDIKRF93kWssm5sec1 38 | iOTV+XXdtxbeaFj/x7CwvrGiiRkM64kyEhSXFj+DlEzQLz9nZsaGJ669sYw6PTMU 39 | rYdtFBrxWFMoS3RpEYsHhP9LNDfglTBjclU83lTeJ2Jlse3qo8OHQCgxsjGh8x97 40 | GoB50lycKvgrJ2ay6gSkPC22j8POdL498G3NIFBbZuk2WHYAFfeJyL8Bf2LdWNkK 41 | 9ckam/MjWFdHvbOzCI5uPO4UqgOR5m2zarH627fW08d0m1rK1gx2+CwJgp6Eqz+V 42 | 1xeCDGKRqe1UzLMLcnR9JrTY7lz+qx1w8WB+3Yoy+RBgLRz0429Yq3JTTV6IIjQK 43 | 29OqYlPimCFMJZSebfgra+gFfuj8ebMu+wohAjIUXjSZcemvOgc+/EkpmpIRyWT1 44 | 4IPsOQku+E5cjeogcNBuUaXqMDHJP6vsw9rxOHOKg6jpTHMYJe2r2aOgMNElrFEs 45 | dDualK/cy1XiVubaLJV+pp6TAABUrIHhYQEzzUn1FlJ36tpFbPxfro7wXuOplUyO 46 | TqX7VPLT3GDThlQb6fnuEJ5nruGGiZJmlQV3R/h7FV7eda/QltpnrSHOIH7ZbLEi 47 | oSxYQBgtBkJHu0up9LpHoDcQZJpofmBcqkH3hJS0DS1OhPmY80U4+v914fWkxMHe 48 | dLz7SzNCnymVx4UFNGEtqkMwGuQpX+eV1Kicp+J/1dHZJgcKYoEuUI64p3L+D6Os 49 | xpVaZWB5B/zZG7zg2QKjU6HMo/wPLJhYPs93RegO3PMAc5doHkcfNQVl1M9tlwI4 50 | rmM3ioKPVvNx7i8wO5bOYrzLOyadoWtkxJUWvmeCyd5I9WNbQVAd6zIeI36TVMtj 51 | aGVmrBBv+4gqQC5D0aQvsw0n3AkqhPQ3F+gmoHFIemSSXIeS5y8Zks9Z7sgUcK2T 52 | SJBfE0KEpGWA7bRhlJDyXiTFIEQnQ7iJ7c1xPj4KCNPJ8nrjJlW8gvgru6yXFpwj 53 | SZTtob9khVfoERx2W1hTvh8KYA524v1G1nZ+/ZsjsgVU9e2lPM8GrXfv08s30etB 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /config/jwt/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwKA5a5B+t18nNeiTvyTw 3 | Azr7LMFnwPr22m92l8ZmtgTcBivakE9hgQ2vZWtPD6YKz2PdLsaPnRxuWl+DlrOG 4 | K+hV48yBFI0v/UNm4pLqeZwSvR83GmMaFhasGGqdCsGvo/nEHBis6wCthmHVcc0d 5 | 0B+bQVVjswvePscVlcSoaekzDO+graPjLpYL6y9gFBDfRRKnZHdSzWygnoAUAfrS 6 | 8VDkjguEhmyr53VNgpgE/Fvn1i3zXpQmJ7/my6LxizMzmfg/uHYB42ZsK51BOcKj 7 | i8sABdSULv+kEmAx3qlau6d+i8Uj77M6O7bp6aMRzTyRSTAfCHMoWaqFzashU4Bd 8 | tNGiBNg5bG/liWnI1JP4IRKy1DjZXl93qiZor3NWdYMMDiUl2omFyYrTg51rECCG 9 | hXluTHv2ETcGtDemPbJB5MNGoSsZZ5BChUf3lk5x0wVfpjnwtxaVWbj8K1qdnStI 10 | AXLg7GUNiPzDb2vLydB5AkpnfOEDMRd/3S1QzEznHLQSMreLv/YOvfuwVb6tFLq1 11 | qtNQ+vAnFQdOabjLOYaj4MtEHIgb41POvYyoODAiGTHHMoKCPonmNqa3ZYVZlx7N 12 | mVwsTAss/txeSbxAcYWpWAkwH1FO7aPDuBkA79BKbNTlydDOvt8LzowZAGukL8pB 13 | sOYBWxwp1p5QS9LodlOsb8sCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /config/packages/api_platform.yaml: -------------------------------------------------------------------------------- 1 | api_platform: 2 | mapping: 3 | paths: ["%kernel.project_dir%/src/Entity"] 4 | collection: 5 | pagination: 6 | enabled: false 7 | items_per_page: 5 8 | client_enabled: true 9 | client_items_per_page: true 10 | items_per_page_parameter_name: "count" 11 | -------------------------------------------------------------------------------- /config/packages/assets.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | assets: 3 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 4 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Put the unique name of your app here: the prefix seed 4 | # is used to compute stable namespaces for cache keys. 5 | #prefix_seed: your_vendor_name/app_name 6 | 7 | # The app cache caches to the filesystem by default. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: ~ 20 | -------------------------------------------------------------------------------- /config/packages/dev/debug.yaml: -------------------------------------------------------------------------------- 1 | debug: 2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 3 | # See the "server:dump" command to start a new server. 4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 5 | -------------------------------------------------------------------------------- /config/packages/dev/easy_log_handler.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | EasyCorp\EasyLog\EasyLogHandler: 3 | public: false 4 | arguments: ['%kernel.logs_dir%/%kernel.environment%.log'] 5 | 6 | #// FIXME: How to add this configuration automatically without messing up with the monolog configuration? 7 | #monolog: 8 | # handlers: 9 | # buffered: 10 | # type: buffer 11 | # handler: easylog 12 | # channels: ['!event'] 13 | # level: debug 14 | # easylog: 15 | # type: service 16 | # id: EasyCorp\EasyLog\EasyLogHandler 17 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | # uncomment to get logging in your browser 9 | # you may have to allow bigger header sizes in your Web server configuration 10 | #firephp: 11 | # type: firephp 12 | # level: info 13 | #chromephp: 14 | # type: chromephp 15 | # level: info 16 | console: 17 | type: console 18 | process_psr_3_messages: false 19 | channels: ["!event", "!doctrine", "!console"] 20 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/dev/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | # See https://symfony.com/doc/current/email/dev_environment.html 2 | swiftmailer: 3 | # send all emails to a specific address 4 | #delivery_addresses: ['me@example.com'] 5 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | # Adds a fallback DATABASE_URL if the env var is not set. 3 | # This allows you to run cache:warmup even if your 4 | # environment variables are not available yet. 5 | # You should not need to change this value. 6 | env(DATABASE_URL): '' 7 | 8 | doctrine: 9 | dbal: 10 | # configure these for your database server 11 | driver: 'pdo_mysql' 12 | server_version: '5.7' 13 | charset: utf8mb4 14 | default_table_options: 15 | charset: utf8mb4 16 | collate: utf8mb4_unicode_ci 17 | 18 | url: '%env(resolve:DATABASE_URL)%' 19 | orm: 20 | auto_generate_proxy_classes: true 21 | naming_strategy: doctrine.orm.naming_strategy.underscore 22 | auto_mapping: true 23 | mappings: 24 | App: 25 | is_bundle: false 26 | type: annotation 27 | dir: '%kernel.project_dir%/src/Entity' 28 | prefix: 'App\Entity' 29 | alias: App 30 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | dir_name: '%kernel.project_dir%/src/Migrations' 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | namespace: DoctrineMigrations 6 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #default_locale: en 4 | #csrf_protection: true 5 | #http_method_override: true 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: ~ 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | -------------------------------------------------------------------------------- /config/packages/lexik_jwt_authentication.yaml: -------------------------------------------------------------------------------- 1 | lexik_jwt_authentication: 2 | secret_key: "%env(resolve:JWT_SECRET_KEY)%" 3 | public_key: "%env(resolve:JWT_PUBLIC_KEY)%" 4 | pass_phrase: "%env(JWT_PASSPHRASE)%" 5 | -------------------------------------------------------------------------------- /config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | defaults: 3 | origin_regex: true 4 | allow_origin: ["%env(CORS_ALLOW_ORIGIN)%"] 5 | allow_methods: ["GET", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"] 6 | allow_headers: ["Content-Type", "Authorization"] 7 | expose_headers: ["Link"] 8 | max_age: 3600 9 | paths: 10 | "^/": ~ 11 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | auto_generate_proxy_classes: false 4 | metadata_cache_driver: 5 | type: service 6 | id: doctrine.system_cache_provider 7 | query_cache_driver: 8 | type: service 9 | id: doctrine.system_cache_provider 10 | result_cache_driver: 11 | type: service 12 | id: doctrine.result_cache_provider 13 | 14 | services: 15 | doctrine.result_cache_provider: 16 | class: Symfony\Component\Cache\DoctrineProvider 17 | public: false 18 | arguments: 19 | - '@doctrine.result_cache_pool' 20 | doctrine.system_cache_provider: 21 | class: Symfony\Component\Cache\DoctrineProvider 22 | public: false 23 | arguments: 24 | - '@doctrine.system_cache_pool' 25 | 26 | framework: 27 | cache: 28 | pools: 29 | doctrine.result_cache_pool: 30 | adapter: cache.app 31 | doctrine.system_cache_pool: 32 | adapter: cache.system 33 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_404s: 8 | # regex: exclude all 404 errors from the logs 9 | - ^/ 10 | nested: 11 | type: stream 12 | # path: "%kernel.logs_dir%/%kernel.environment%.log" 13 | path: "php://stderr" 14 | level: debug 15 | console: 16 | type: console 17 | process_psr_3_messages: false 18 | channels: ["!event", "!doctrine"] 19 | deprecation: 20 | type: stream 21 | path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log" 22 | deprecation_filter: 23 | type: filter 24 | handler: deprecation 25 | max_level: info 26 | channels: ["php"] 27 | -------------------------------------------------------------------------------- /config/packages/prod/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | #webpack_encore: 2 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 3 | # Available in version 1.2 4 | #cache: true 5 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | utf8: true 5 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | encoders: 3 | App\Entity\User: 4 | algorithm: argon2i 5 | 6 | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers 7 | providers: 8 | # used to reload user from session & other features (e.g. switch_user) 9 | app_user_provider: 10 | entity: 11 | class: App\Entity\User 12 | property: email 13 | firewalls: 14 | dev: 15 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 16 | security: false 17 | registration: 18 | pattern: ^/api/users 19 | anonymous: true 20 | stateless: true 21 | methods: [POST] 22 | login: 23 | pattern: ^/api/login 24 | stateless: true 25 | anonymous: true 26 | json_login: 27 | check_path: /api/login_check 28 | success_handler: lexik_jwt_authentication.handler.authentication_success 29 | failure_handler: lexik_jwt_authentication.handler.authentication_failure 30 | api: 31 | pattern: ^/api 32 | stateless: true 33 | anonymous: true 34 | guard: 35 | authenticators: 36 | - lexik_jwt_authentication.jwt_token_authenticator 37 | main: 38 | anonymous: true 39 | # activate different ways to authenticate 40 | # http_basic: true 41 | # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate 42 | # form_login: true 43 | # https://symfony.com/doc/current/security/form_login_setup.html 44 | 45 | # Easy way to control access for large sections of your site 46 | # Note: Only the *first* access control that matches will be used 47 | access_control: 48 | - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } 49 | - { path: ^/api/customers, roles: IS_AUTHENTICATED_FULLY } 50 | - { path: ^/api/invoices, roles: IS_AUTHENTICATED_FULLY } 51 | - { 52 | path: ^/api/users, 53 | roles: IS_AUTHENTICATED_FULLY, 54 | methods: [GET, PUT, DELETE], 55 | } 56 | # - { path: ^/admin, roles: ROLE_ADMIN } 57 | # - { path: ^/profile, roles: ROLE_USER } 58 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | url: '%env(MAILER_URL)%' 3 | spool: { type: 'memory' } 4 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | -------------------------------------------------------------------------------- /config/packages/test/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/test/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | disable_delivery: true 3 | -------------------------------------------------------------------------------- /config/packages/test/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | # As of Symfony 4.3 you can disable the NotCompromisedPassword Validator 4 | # disable_not_compromised_password: true 5 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: '%locale%' 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - '%locale%' 7 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | -------------------------------------------------------------------------------- /config/packages/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | # The path where Encore is building the assets. 3 | # This should match Encore.setOutputPath() in webpack.config.js. 4 | output_path: '%kernel.project_dir%/public/build' 5 | # If multiple builds are defined (as shown below), you can disable the default build: 6 | # output_path: false 7 | 8 | # if using Encore.enableIntegrityHashes() specify the crossorigin attribute value (default: false, or use 'anonymous' or 'use-credentials') 9 | # crossorigin: 'anonymous' 10 | 11 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 12 | # Available in version 1.2 13 | #cache: '%kernel.debug%' 14 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | #index: 2 | # path: / 3 | # controller: App\Controller\DefaultController::index 4 | api_login_check: 5 | path: /api/login_check 6 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/Controller/ 3 | type: annotation 4 | -------------------------------------------------------------------------------- /config/routes/api_platform.yaml: -------------------------------------------------------------------------------- 1 | api_platform: 2 | resource: . 3 | type: api_platform 4 | prefix: /api 5 | -------------------------------------------------------------------------------- /config/routes/dev/twig.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@TwigBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 6 | parameters: 7 | locale: "en" 8 | 9 | services: 10 | # default configuration for services in *this* file 11 | _defaults: 12 | autowire: true # Automatically injects dependencies in your services. 13 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 14 | 15 | # makes classes in src/ available to be used as services 16 | # this creates a service per class whose id is the fully-qualified class name 17 | App\: 18 | resource: "../src/*" 19 | exclude: "../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}" 20 | 21 | # controllers are imported separately to make sure services can be injected 22 | # as action arguments even if you don't extend any base controller class 23 | App\Controller\: 24 | resource: "../src/Controller" 25 | tags: ["controller.service_arguments"] 26 | # add more service definitions when explicit configuration is needed 27 | # please note that last definitions always *replace* previous ones 28 | 29 | App\Serializer\PatchedDateTimeNormalizer: 30 | tags: [serializer.normalizer] 31 | App\Events\JwtCreatedSubscriber: 32 | tags: 33 | - { 34 | name: kernel.event_listener, 35 | event: lexik_jwt_authentication.on_jwt_created, 36 | method: updateJwtData, 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@babel/preset-react": "^7.0.0", 4 | "@symfony/webpack-encore": "^0.27.0", 5 | "core-js": "^3.0.0", 6 | "webpack-notifier": "^1.6.0" 7 | }, 8 | "license": "UNLICENSED", 9 | "private": true, 10 | "scripts": { 11 | "dev-server": "encore dev-server --port 8080", 12 | "dev": "encore dev", 13 | "watch": "encore dev --watch", 14 | "build": "encore production --progress" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.18.0", 18 | "dotenv": "^8.0.0", 19 | "fs": "0.0.1-security", 20 | "jwt-decode": "^2.2.0", 21 | "moment": "^2.24.0", 22 | "react": "^16.8.6", 23 | "react-content-loader": "^4.2.1", 24 | "react-dom": "^16.8.6", 25 | "react-router-dom": "^5.0.0", 26 | "react-toastify": "^5.2.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | tests 19 | 20 | 21 | 22 | 23 | 24 | src 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | # Use the front controller as index file. It serves as a fallback solution when 2 | # every other rewrite/redirect fails (e.g. in an aliased environment without 3 | # mod_rewrite). Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex index.php 7 | 8 | # By default, Apache does not evaluate symbolic links if you did not enable this 9 | # feature in your server configuration. Uncomment the following line if you 10 | # install assets as symlinks or if you experience problems related to symlinks 11 | # when compiling LESS/Sass/CoffeScript assets. 12 | # Options FollowSymlinks 13 | 14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve 15 | # to the front controller "/index.php" but be rewritten to "/index.php/index". 16 | 17 | Options -MultiViews 18 | 19 | 20 | 21 | RewriteEngine On 22 | 23 | # Determine the RewriteBase automatically and set it as environment variable. 24 | # If you are using Apache aliases to do mass virtual hosting or installed the 25 | # project in a subdirectory, the base path will be prepended to allow proper 26 | # resolution of the index.php file and to redirect to the correct URI. It will 27 | # work in environments without path prefix as well, providing a safe, one-size 28 | # fits all solution. But as you do not need it in this case, you can comment 29 | # the following 2 lines to eliminate the overhead. 30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 31 | RewriteRule ^(.*) - [E=BASE:%1] 32 | 33 | # Sets the HTTP_AUTHORIZATION header removed by Apache 34 | RewriteCond %{HTTP:Authorization} . 35 | RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 36 | 37 | # Redirect to URI without front controller to prevent duplicate content 38 | # (with and without `/index.php`). Only do this redirect on the initial 39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 40 | # endless redirect loop (request -> rewrite to front controller -> 41 | # redirect -> request -> ...). 42 | # So in case you get a "too many redirects" error or you always get redirected 43 | # to the start page because your Apache does not expose the REDIRECT_STATUS 44 | # environment variable, you have 2 choices: 45 | # - disable this feature by commenting the following 2 lines or 46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 47 | # following RewriteCond (best solution) 48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 49 | RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] 50 | 51 | # If the requested filename exists, simply serve it. 52 | # We only want to let Apache serve files and not directories. 53 | RewriteCond %{REQUEST_FILENAME} -f 54 | RewriteRule ^ - [L] 55 | 56 | # Rewrite all other queries to the front controller. 57 | RewriteRule ^ %{ENV:BASE}/index.php [L] 58 | 59 | 60 | 61 | 62 | # When mod_rewrite is not available, we instruct a temporary redirect of 63 | # the start page to the front controller explicitly so that the website 64 | # and the generated links can still be used. 65 | RedirectMatch 307 ^/$ /index.php/ 66 | # RedirectTemp cannot be used instead 67 | 68 | 69 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 26 | $response->send(); 27 | $kernel->terminate($request, $response); 28 | -------------------------------------------------------------------------------- /src/Controller/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liorchamla/formation-api-platform-react/599aeff7496c6c3a573501a36c961b1f05a62338/src/Controller/.gitignore -------------------------------------------------------------------------------- /src/Controller/AppController.php: -------------------------------------------------------------------------------- 1 | render('app/index.html.twig', []); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Controller/InvoiceIncrementationController.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 17 | } 18 | 19 | public function __invoke(Invoice $data) 20 | { 21 | $data->setChrono($data->getChrono() + 1); 22 | 23 | $this->manager->flush(); 24 | 25 | return $data; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/DataFixtures/AppFixtures.php: -------------------------------------------------------------------------------- 1 | encoder = $encoder; 25 | } 26 | 27 | public function load(ObjectManager $manager) 28 | { 29 | $faker = Factory::create('fr_FR'); 30 | 31 | for ($u = 0; $u < 10; $u++) { 32 | $user = new User(); 33 | 34 | $chrono = 1; 35 | 36 | $hash = $this->encoder->encodePassword($user, "password"); 37 | 38 | $user->setFirstName($faker->firstName()) 39 | ->setLastName($faker->lastName) 40 | ->setEmail($faker->email) 41 | ->setPassword($hash); 42 | 43 | $manager->persist($user); 44 | 45 | for ($c = 0; $c < mt_rand(5, 20); $c++) { 46 | $customer = new Customer(); 47 | $customer->setFirstName($faker->firstName()) 48 | ->setLastName($faker->lastName) 49 | ->setCompany($faker->company) 50 | ->setEmail($faker->email) 51 | ->setUser($user); 52 | 53 | $manager->persist($customer); 54 | 55 | for ($i = 0; $i < mt_rand(3, 10); $i++) { 56 | $invoice = new Invoice(); 57 | $invoice->setAmount($faker->randomFloat(2, 250, 5000)) 58 | ->setSentAt($faker->dateTimeBetween('-6 months')) 59 | ->setStatus($faker->randomElement(['SENT', 'PAID', 'CANCELLED'])) 60 | ->setCustomer($customer) 61 | ->setChrono($chrono); 62 | 63 | $chrono++; 64 | 65 | $manager->persist($invoice); 66 | } 67 | } 68 | } 69 | 70 | $manager->flush(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Doctrine/CurrentUserExtension.php: -------------------------------------------------------------------------------- 1 | security = $security; 23 | $this->auth = $checker; 24 | } 25 | 26 | private function addWhere(QueryBuilder $queryBuilder, string $resourceClass) 27 | { 28 | // 1. Obtenir l'utilisateur connecté 29 | $user = $this->security->getUser(); 30 | 31 | // 2. Si on demande des invoices ou des customers alors, agir sur la requête pour qu'elle tienne compte de l'utilisateur connecté 32 | if ( 33 | ($resourceClass === Customer::class || $resourceClass === Invoice::class) 34 | && 35 | !$this->auth->isGranted('ROLE_ADMIN') 36 | && 37 | $user instanceof User 38 | ) { 39 | $rootAlias = $queryBuilder->getRootAliases()[0]; 40 | 41 | if ($resourceClass === Customer::class) { 42 | $queryBuilder->andWhere("$rootAlias.user = :user"); 43 | } else if ($resourceClass === Invoice::class) { 44 | $queryBuilder->join("$rootAlias.customer", "c") 45 | ->andWhere("c.user = :user"); 46 | } 47 | 48 | $queryBuilder->setParameter("user", $user); 49 | } 50 | } 51 | 52 | public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?string $operationName = null) 53 | { 54 | $this->addWhere($queryBuilder, $resourceClass); 55 | } 56 | 57 | public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?string $operationName = null, array $context = []) 58 | { 59 | $this->addWhere($queryBuilder, $resourceClass); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Entity/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liorchamla/formation-api-platform-react/599aeff7496c6c3a573501a36c961b1f05a62338/src/Entity/.gitignore -------------------------------------------------------------------------------- /src/Entity/Customer.php: -------------------------------------------------------------------------------- 1 | invoices = new ArrayCollection(); 90 | } 91 | 92 | /** 93 | * Permet de récupérer le total des invoices 94 | * @Groups({"customers_read"}) 95 | * @return float 96 | */ 97 | public function getTotalAmount(): float 98 | { 99 | return array_reduce($this->invoices->toArray(), function ($total, $invoice) { 100 | return $total + $invoice->getAmount(); 101 | }, 0); 102 | } 103 | 104 | /** 105 | * Récupérer le montant total non payé (montant total hors factures payées ou annulées) 106 | * @Groups({"customers_read"}) 107 | * @return float 108 | */ 109 | public function getUnpaidAmount(): float 110 | { 111 | return array_reduce($this->invoices->toArray(), function ($total, $invoice) { 112 | return $total + ($invoice->getStatus() === "PAID" || $invoice->getStatus() === "CANCELLED" ? 0 : $invoice->getAmount()); 113 | }, 0); 114 | } 115 | 116 | public function getId(): ?int 117 | { 118 | return $this->id; 119 | } 120 | 121 | public function getFirstName(): ?string 122 | { 123 | return $this->firstName; 124 | } 125 | 126 | public function setFirstName(string $firstName): self 127 | { 128 | $this->firstName = $firstName; 129 | 130 | return $this; 131 | } 132 | 133 | public function getLastName(): ?string 134 | { 135 | return $this->lastName; 136 | } 137 | 138 | public function setLastName(string $lastName): self 139 | { 140 | $this->lastName = $lastName; 141 | 142 | return $this; 143 | } 144 | 145 | public function getEmail(): ?string 146 | { 147 | return $this->email; 148 | } 149 | 150 | public function setEmail(string $email): self 151 | { 152 | $this->email = $email; 153 | 154 | return $this; 155 | } 156 | 157 | public function getCompany(): ?string 158 | { 159 | return $this->company; 160 | } 161 | 162 | public function setCompany(?string $company): self 163 | { 164 | $this->company = $company; 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * @return Collection|Invoice[] 171 | */ 172 | public function getInvoices(): Collection 173 | { 174 | return $this->invoices; 175 | } 176 | 177 | public function addInvoice(Invoice $invoice): self 178 | { 179 | if (!$this->invoices->contains($invoice)) { 180 | $this->invoices[] = $invoice; 181 | $invoice->setCustomer($this); 182 | } 183 | 184 | return $this; 185 | } 186 | 187 | public function removeInvoice(Invoice $invoice): self 188 | { 189 | if ($this->invoices->contains($invoice)) { 190 | $this->invoices->removeElement($invoice); 191 | // set the owning side to null (unless already changed) 192 | if ($invoice->getCustomer() === $this) { 193 | $invoice->setCustomer(null); 194 | } 195 | } 196 | 197 | return $this; 198 | } 199 | 200 | public function getUser(): ?User 201 | { 202 | return $this->user; 203 | } 204 | 205 | public function setUser(?User $user): self 206 | { 207 | $this->user = $user; 208 | 209 | return $this; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Entity/Invoice.php: -------------------------------------------------------------------------------- 1 | customer->getUser(); 99 | } 100 | 101 | public function getId(): ?int 102 | { 103 | return $this->id; 104 | } 105 | 106 | public function getAmount(): ?float 107 | { 108 | return $this->amount; 109 | } 110 | 111 | public function setAmount($amount): self 112 | { 113 | $this->amount = $amount; 114 | 115 | return $this; 116 | } 117 | 118 | public function getSentAt(): ?\DateTimeInterface 119 | { 120 | return $this->sentAt; 121 | } 122 | 123 | public function setSentAt($sentAt): self 124 | { 125 | $this->sentAt = $sentAt; 126 | 127 | return $this; 128 | } 129 | 130 | public function getStatus(): ?string 131 | { 132 | return $this->status; 133 | } 134 | 135 | public function setStatus(string $status): self 136 | { 137 | $this->status = $status; 138 | 139 | return $this; 140 | } 141 | 142 | public function getCustomer(): ?Customer 143 | { 144 | return $this->customer; 145 | } 146 | 147 | public function setCustomer(?Customer $customer): self 148 | { 149 | $this->customer = $customer; 150 | 151 | return $this; 152 | } 153 | 154 | public function getChrono(): ?int 155 | { 156 | return $this->chrono; 157 | } 158 | 159 | public function setChrono($chrono): self 160 | { 161 | $this->chrono = $chrono; 162 | 163 | return $this; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Entity/User.php: -------------------------------------------------------------------------------- 1 | customers = new ArrayCollection(); 76 | } 77 | 78 | public function getId(): ?int 79 | { 80 | return $this->id; 81 | } 82 | 83 | public function getEmail(): ?string 84 | { 85 | return $this->email; 86 | } 87 | 88 | public function setEmail(string $email): self 89 | { 90 | $this->email = $email; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * A visual identifier that represents this user. 97 | * 98 | * @see UserInterface 99 | */ 100 | public function getUsername(): string 101 | { 102 | return (string)$this->email; 103 | } 104 | 105 | /** 106 | * @see UserInterface 107 | */ 108 | public function getRoles(): array 109 | { 110 | $roles = $this->roles; 111 | // guarantee every user at least has ROLE_USER 112 | $roles[] = 'ROLE_USER'; 113 | 114 | return array_unique($roles); 115 | } 116 | 117 | public function setRoles(array $roles): self 118 | { 119 | $this->roles = $roles; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * @see UserInterface 126 | */ 127 | public function getPassword(): string 128 | { 129 | return (string)$this->password; 130 | } 131 | 132 | public function setPassword(string $password): self 133 | { 134 | $this->password = $password; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * @see UserInterface 141 | */ 142 | public function getSalt() 143 | { 144 | // not needed when using the "bcrypt" algorithm in security.yaml 145 | } 146 | 147 | /** 148 | * @see UserInterface 149 | */ 150 | public function eraseCredentials() 151 | { 152 | // If you store any temporary, sensitive data on the user, clear it here 153 | // $this->plainPassword = null; 154 | } 155 | 156 | public function getFirstName(): ?string 157 | { 158 | return $this->firstName; 159 | } 160 | 161 | public function setFirstName(string $firstName): self 162 | { 163 | $this->firstName = $firstName; 164 | 165 | return $this; 166 | } 167 | 168 | public function getLastName(): ?string 169 | { 170 | return $this->lastName; 171 | } 172 | 173 | public function setLastName(string $lastName): self 174 | { 175 | $this->lastName = $lastName; 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * @return Collection|Customer[] 182 | */ 183 | public function getCustomers(): Collection 184 | { 185 | return $this->customers; 186 | } 187 | 188 | public function addCustomer(Customer $customer): self 189 | { 190 | if (!$this->customers->contains($customer)) { 191 | $this->customers[] = $customer; 192 | $customer->setUser($this); 193 | } 194 | 195 | return $this; 196 | } 197 | 198 | public function removeCustomer(Customer $customer): self 199 | { 200 | if ($this->customers->contains($customer)) { 201 | $this->customers->removeElement($customer); 202 | // set the owning side to null (unless already changed) 203 | if ($customer->getUser() === $this) { 204 | $customer->setUser(null); 205 | } 206 | } 207 | 208 | return $this; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Events/CustomerUserSubscriber.php: -------------------------------------------------------------------------------- 1 | security = $security; 20 | } 21 | 22 | public static function getSubscribedEvents() 23 | { 24 | return [ 25 | KernelEvents::VIEW => ['setUserForCustomer', EventPriorities::PRE_VALIDATE] 26 | ]; 27 | } 28 | 29 | public function setUserForCustomer(GetResponseForControllerResultEvent $event) 30 | { 31 | $customer = $event->getControllerResult(); 32 | $method = $event->getRequest()->getMethod(); 33 | 34 | if ($customer instanceof Customer && $method === "POST") { 35 | // Choper l'utilisateur actuellement connecté 36 | $user = $this->security->getUser(); 37 | // Assigner l'utilisateur au Customer qu'on est en train de créer 38 | if ($user) { 39 | $customer->setUser($user); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Events/InvoiceChronoSubscriber.php: -------------------------------------------------------------------------------- 1 | security = $security; 21 | $this->repository = $repository; 22 | } 23 | 24 | public static function getSubscribedEvents() 25 | { 26 | return [ 27 | KernelEvents::VIEW => ['setChronoForInvoice', EventPriorities::PRE_VALIDATE] 28 | ]; 29 | } 30 | 31 | public function setChronoForInvoice(GetResponseForControllerResultEvent $event) 32 | { 33 | $invoice = $event->getControllerResult(); 34 | $method = $event->getRequest()->getMethod(); 35 | 36 | if ($invoice instanceof Invoice && $method === "POST") { 37 | $nextChrono = $this->repository->findNextChrono($this->security->getUser()); 38 | $invoice->setChrono($nextChrono); 39 | 40 | // TODO : A déplacer dans une classe dédiée 41 | if (empty($invoice->getSentAt())) { 42 | $invoice->setSentAt(new \DateTime()); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Events/JwtCreatedSubscriber.php: -------------------------------------------------------------------------------- 1 | getUser(); 13 | 14 | // 2. Enrichir les data pour qu'elles contiennent ces données 15 | $data = $event->getData(); 16 | $data['firstName'] = $user->getFirstName(); 17 | $data['lastName'] = $user->getLastName(); 18 | 19 | $event->setData($data); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Events/PasswordEncoderSubscriber.php: -------------------------------------------------------------------------------- 1 | encoder = $encoder; 21 | } 22 | 23 | public static function getSubscribedEvents() 24 | { 25 | return [ 26 | KernelEvents::VIEW => ['encodePassword', EventPriorities::PRE_WRITE] 27 | ]; 28 | } 29 | 30 | public function encodePassword(GetResponseForControllerResultEvent $event) 31 | { 32 | $user = $event->getControllerResult(); 33 | $method = $event->getRequest()->getMethod(); // POST, GET, PUT, ... 34 | 35 | if ($user instanceof User && $method === "POST") { 36 | $hash = $this->encoder->encodePassword($user, $user->getPassword()); 37 | $user->setPassword($hash); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | getProjectDir().'/config/bundles.php'; 21 | foreach ($contents as $class => $envs) { 22 | if ($envs[$this->environment] ?? $envs['all'] ?? false) { 23 | yield new $class(); 24 | } 25 | } 26 | } 27 | 28 | public function getProjectDir(): string 29 | { 30 | return \dirname(__DIR__); 31 | } 32 | 33 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void 34 | { 35 | $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); 36 | $container->setParameter('container.dumper.inline_class_loader', true); 37 | $confDir = $this->getProjectDir().'/config'; 38 | 39 | $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); 40 | $loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); 41 | $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); 42 | $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); 43 | } 44 | 45 | protected function configureRoutes(RouteCollectionBuilder $routes): void 46 | { 47 | $confDir = $this->getProjectDir().'/config'; 48 | 49 | $routes->import($confDir.'/{routes}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); 50 | $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); 51 | $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liorchamla/formation-api-platform-react/599aeff7496c6c3a573501a36c961b1f05a62338/src/Migrations/.gitignore -------------------------------------------------------------------------------- /src/Migrations/Version20190424130800.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 24 | 25 | $this->addSql('CREATE TABLE customer (id INT AUTO_INCREMENT NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, company VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 26 | } 27 | 28 | public function down(Schema $schema) : void 29 | { 30 | // this down() migration is auto-generated, please modify it to your needs 31 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 32 | 33 | $this->addSql('DROP TABLE customer'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Migrations/Version20190424131425.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 24 | 25 | $this->addSql('CREATE TABLE invoice (id INT AUTO_INCREMENT NOT NULL, customer_id INT NOT NULL, amount DOUBLE PRECISION NOT NULL, sent_at DATETIME NOT NULL, status VARCHAR(255) NOT NULL, INDEX IDX_906517449395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 26 | $this->addSql('ALTER TABLE invoice ADD CONSTRAINT FK_906517449395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id)'); 27 | } 28 | 29 | public function down(Schema $schema) : void 30 | { 31 | // this down() migration is auto-generated, please modify it to your needs 32 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 33 | 34 | $this->addSql('DROP TABLE invoice'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Migrations/Version20190424132935.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 24 | 25 | $this->addSql('ALTER TABLE invoice ADD chrono INT NOT NULL'); 26 | } 27 | 28 | public function down(Schema $schema) : void 29 | { 30 | // this down() migration is auto-generated, please modify it to your needs 31 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 32 | 33 | $this->addSql('ALTER TABLE invoice DROP chrono'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Migrations/Version20190424133911.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 24 | 25 | $this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); 26 | $this->addSql('ALTER TABLE customer ADD user_id INT DEFAULT NULL'); 27 | $this->addSql('ALTER TABLE customer ADD CONSTRAINT FK_81398E09A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); 28 | $this->addSql('CREATE INDEX IDX_81398E09A76ED395 ON customer (user_id)'); 29 | } 30 | 31 | public function down(Schema $schema) : void 32 | { 33 | // this down() migration is auto-generated, please modify it to your needs 34 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 35 | 36 | $this->addSql('ALTER TABLE customer DROP FOREIGN KEY FK_81398E09A76ED395'); 37 | $this->addSql('DROP TABLE user'); 38 | $this->addSql('DROP INDEX IDX_81398E09A76ED395 ON customer'); 39 | $this->addSql('ALTER TABLE customer DROP user_id'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Repository/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liorchamla/formation-api-platform-react/599aeff7496c6c3a573501a36c961b1f05a62338/src/Repository/.gitignore -------------------------------------------------------------------------------- /src/Repository/CustomerRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('c') 29 | ->andWhere('c.exampleField = :val') 30 | ->setParameter('val', $value) 31 | ->orderBy('c.id', 'ASC') 32 | ->setMaxResults(10) 33 | ->getQuery() 34 | ->getResult() 35 | ; 36 | } 37 | */ 38 | 39 | /* 40 | public function findOneBySomeField($value): ?Customer 41 | { 42 | return $this->createQueryBuilder('c') 43 | ->andWhere('c.exampleField = :val') 44 | ->setParameter('val', $value) 45 | ->getQuery() 46 | ->getOneOrNullResult() 47 | ; 48 | } 49 | */ 50 | } 51 | -------------------------------------------------------------------------------- /src/Repository/InvoiceRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder("i") 27 | ->select("i.chrono") 28 | ->join("i.customer", "c") 29 | ->where("c.user = :user") 30 | ->setParameter("user", $user) 31 | ->orderBy("i.chrono", "DESC") 32 | ->setMaxResults(1) 33 | ->getQuery() 34 | ->getSingleScalarResult() + 1; 35 | } catch (\Exception $e) { 36 | return 1; 37 | } 38 | } 39 | 40 | // /** 41 | // * @return Invoice[] Returns an array of Invoice objects 42 | // */ 43 | /* 44 | public function findByExampleField($value) 45 | { 46 | return $this->createQueryBuilder('i') 47 | ->andWhere('i.exampleField = :val') 48 | ->setParameter('val', $value) 49 | ->orderBy('i.id', 'ASC') 50 | ->setMaxResults(10) 51 | ->getQuery() 52 | ->getResult() 53 | ; 54 | } 55 | */ 56 | 57 | /* 58 | public function findOneBySomeField($value): ?Invoice 59 | { 60 | return $this->createQueryBuilder('i') 61 | ->andWhere('i.exampleField = :val') 62 | ->setParameter('val', $value) 63 | ->getQuery() 64 | ->getOneOrNullResult() 65 | ; 66 | } 67 | */ 68 | } 69 | -------------------------------------------------------------------------------- /src/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('u') 29 | ->andWhere('u.exampleField = :val') 30 | ->setParameter('val', $value) 31 | ->orderBy('u.id', 'ASC') 32 | ->setMaxResults(10) 33 | ->getQuery() 34 | ->getResult() 35 | ; 36 | } 37 | */ 38 | 39 | /* 40 | public function findOneBySomeField($value): ?User 41 | { 42 | return $this->createQueryBuilder('u') 43 | ->andWhere('u.exampleField = :val') 44 | ->setParameter('val', $value) 45 | ->getQuery() 46 | ->getOneOrNullResult() 47 | ; 48 | } 49 | */ 50 | } 51 | -------------------------------------------------------------------------------- /src/Serializer/PatchedDateTimeNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Serializer; 13 | 14 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 15 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; 16 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 17 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 18 | use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; 19 | 20 | /** 21 | * Normalizes an object implementing the {@see \DateTimeInterface} to a date string. 22 | * Denormalizes a date string to an instance of {@see \DateTime} or {@see \DateTimeImmutable}. 23 | * 24 | * @author Kévin Dunglas 25 | */ 26 | class PatchedDateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface 27 | { 28 | const FORMAT_KEY = 'datetime_format'; 29 | const TIMEZONE_KEY = 'datetime_timezone'; 30 | 31 | private $defaultContext; 32 | 33 | private static $supportedTypes = [ 34 | \DateTimeInterface::class => true, 35 | \DateTimeImmutable::class => true, 36 | \DateTime::class => true, 37 | ]; 38 | 39 | /** 40 | * @param array $defaultContext 41 | */ 42 | public function __construct($defaultContext = [], \DateTimeZone $timezone = null) 43 | { 44 | $this->defaultContext = [ 45 | self::FORMAT_KEY => \DateTime::RFC3339, 46 | self::TIMEZONE_KEY => null, 47 | ]; 48 | 49 | if (!\is_array($defaultContext)) { 50 | @trigger_error('Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.', E_USER_DEPRECATED); 51 | 52 | $defaultContext = [self::FORMAT_KEY => (string)$defaultContext]; 53 | $defaultContext[self::TIMEZONE_KEY] = $timezone; 54 | } 55 | 56 | $this->defaultContext = array_merge($this->defaultContext, $defaultContext); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | * 62 | * @throws InvalidArgumentException 63 | */ 64 | public function normalize($object, $format = null, array $context = []) 65 | { 66 | if (!$object instanceof \DateTimeInterface) { 67 | throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".'); 68 | } 69 | 70 | $dateTimeFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]; 71 | $timezone = $this->getTimezone($context); 72 | 73 | if (null !== $timezone) { 74 | $object = clone $object; 75 | $object = $object->setTimezone($timezone); 76 | } 77 | 78 | return $object->format($dateTimeFormat); 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function supportsNormalization($data, $format = null) 85 | { 86 | return $data instanceof \DateTimeInterface; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | * 92 | * @throws NotNormalizableValueException 93 | */ 94 | public function denormalize($data, $class, $format = null, array $context = []) 95 | { 96 | $dateTimeFormat = $context[self::FORMAT_KEY] ?? null; 97 | $timezone = $this->getTimezone($context); 98 | 99 | if ('' === $data || null === $data) { 100 | throw new NotNormalizableValueException('The data is either an empty string or null, you should pass a string that can be parsed with the passed format or a valid DateTime string.'); 101 | } 102 | 103 | if (null !== $dateTimeFormat) { 104 | $object = \DateTime::class === $class ? \DateTime::createFromFormat($dateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data, $timezone); 105 | 106 | if (false !== $object) { 107 | return $object; 108 | } 109 | 110 | $dateTimeErrors = \DateTime::class === $class ? \DateTime::getLastErrors() : \DateTimeImmutable::getLastErrors(); 111 | 112 | throw new NotNormalizableValueException(sprintf( 113 | 'Parsing datetime string "%s" using format "%s" resulted in %d errors:' . "\n" . '%s', 114 | $data, 115 | $dateTimeFormat, 116 | $dateTimeErrors['error_count'], 117 | implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])) 118 | )); 119 | } 120 | 121 | try { 122 | return \DateTime::class === $class ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone); 123 | } catch (\Exception $e) { 124 | 125 | if ($context['disable_type_enforcement'] ?? false) { 126 | return $data; 127 | } 128 | 129 | throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e); 130 | } 131 | } 132 | 133 | /** 134 | * {@inheritdoc} 135 | */ 136 | public function supportsDenormalization($data, $type, $format = null) 137 | { 138 | return isset(self::$supportedTypes[$type]); 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | */ 144 | public function hasCacheableSupportsMethod(): bool 145 | { 146 | return __CLASS__ === \get_class($this); 147 | } 148 | 149 | /** 150 | * Formats datetime errors. 151 | * 152 | * @return string[] 153 | */ 154 | private function formatDateTimeErrors(array $errors) 155 | { 156 | $formattedErrors = []; 157 | 158 | foreach ($errors as $pos => $message) { 159 | $formattedErrors[] = sprintf('at position %d: %s', $pos, $message); 160 | } 161 | 162 | return $formattedErrors; 163 | } 164 | 165 | private function getTimezone(array $context) 166 | { 167 | $dateTimeZone = $context[self::TIMEZONE_KEY] ?? $this->defaultContext[self::TIMEZONE_KEY]; 168 | 169 | if (null === $dateTimeZone) { 170 | return null; 171 | } 172 | 173 | return $dateTimeZone instanceof \DateTimeZone ? $dateTimeZone : new \DateTimeZone($dateTimeZone); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "api-platform/api-pack": { 3 | "version": "v1.2.0" 4 | }, 5 | "api-platform/core": { 6 | "version": "2.1", 7 | "recipe": { 8 | "repo": "github.com/symfony/recipes", 9 | "branch": "master", 10 | "version": "2.1", 11 | "ref": "18727d8f229306860b46955f438e1897421da689" 12 | }, 13 | "files": [ 14 | "./config/packages/api_platform.yaml", 15 | "./config/routes/api_platform.yaml", 16 | "./src/Entity/.gitignore" 17 | ] 18 | }, 19 | "doctrine/annotations": { 20 | "version": "1.0", 21 | "recipe": { 22 | "repo": "github.com/symfony/recipes", 23 | "branch": "master", 24 | "version": "1.0", 25 | "ref": "cb4152ebcadbe620ea2261da1a1c5a9b8cea7672" 26 | }, 27 | "files": [ 28 | "./config/routes/annotations.yaml" 29 | ] 30 | }, 31 | "doctrine/cache": { 32 | "version": "v1.8.0" 33 | }, 34 | "doctrine/collections": { 35 | "version": "v1.6.1" 36 | }, 37 | "doctrine/common": { 38 | "version": "v2.10.0" 39 | }, 40 | "doctrine/data-fixtures": { 41 | "version": "v1.3.1" 42 | }, 43 | "doctrine/dbal": { 44 | "version": "v2.9.2" 45 | }, 46 | "doctrine/doctrine-bundle": { 47 | "version": "1.6", 48 | "recipe": { 49 | "repo": "github.com/symfony/recipes", 50 | "branch": "master", 51 | "version": "1.6", 52 | "ref": "453e89b78ded666f351617baca5ae40d20622351" 53 | }, 54 | "files": [ 55 | "./config/packages/doctrine.yaml", 56 | "./config/packages/prod/doctrine.yaml", 57 | "./src/Entity/.gitignore", 58 | "./src/Repository/.gitignore" 59 | ] 60 | }, 61 | "doctrine/doctrine-cache-bundle": { 62 | "version": "1.3.5" 63 | }, 64 | "doctrine/doctrine-fixtures-bundle": { 65 | "version": "3.0", 66 | "recipe": { 67 | "repo": "github.com/symfony/recipes", 68 | "branch": "master", 69 | "version": "3.0", 70 | "ref": "fc52d86631a6dfd9fdf3381d0b7e3df2069e51b3" 71 | }, 72 | "files": [ 73 | "./src/DataFixtures/AppFixtures.php" 74 | ] 75 | }, 76 | "doctrine/doctrine-migrations-bundle": { 77 | "version": "1.2", 78 | "recipe": { 79 | "repo": "github.com/symfony/recipes", 80 | "branch": "master", 81 | "version": "1.2", 82 | "ref": "c1431086fec31f17fbcfe6d6d7e92059458facc1" 83 | }, 84 | "files": [ 85 | "./config/packages/doctrine_migrations.yaml", 86 | "./src/Migrations/.gitignore" 87 | ] 88 | }, 89 | "doctrine/event-manager": { 90 | "version": "v1.0.0" 91 | }, 92 | "doctrine/inflector": { 93 | "version": "v1.3.0" 94 | }, 95 | "doctrine/instantiator": { 96 | "version": "1.2.0" 97 | }, 98 | "doctrine/lexer": { 99 | "version": "v1.0.1" 100 | }, 101 | "doctrine/migrations": { 102 | "version": "v2.0.0" 103 | }, 104 | "doctrine/orm": { 105 | "version": "v2.6.3" 106 | }, 107 | "doctrine/persistence": { 108 | "version": "v1.1.0" 109 | }, 110 | "doctrine/reflection": { 111 | "version": "v1.0.0" 112 | }, 113 | "easycorp/easy-log-handler": { 114 | "version": "1.0", 115 | "recipe": { 116 | "repo": "github.com/symfony/recipes", 117 | "branch": "master", 118 | "version": "1.0", 119 | "ref": "70062abc2cd58794d2a90274502f81b55cd9951b" 120 | }, 121 | "files": [ 122 | "./config/packages/dev/easy_log_handler.yaml" 123 | ] 124 | }, 125 | "egulias/email-validator": { 126 | "version": "2.1.7" 127 | }, 128 | "facebook/webdriver": { 129 | "version": "1.6.0" 130 | }, 131 | "fig/link-util": { 132 | "version": "1.0.0" 133 | }, 134 | "fzaninotto/faker": { 135 | "version": "v1.8.0" 136 | }, 137 | "jdorn/sql-formatter": { 138 | "version": "v1.2.17" 139 | }, 140 | "lcobucci/jwt": { 141 | "version": "3.3.1" 142 | }, 143 | "lexik/jwt-authentication-bundle": { 144 | "version": "2.5", 145 | "recipe": { 146 | "repo": "github.com/symfony/recipes", 147 | "branch": "master", 148 | "version": "2.5", 149 | "ref": "5b2157bcd5778166a5696e42f552ad36529a07a6" 150 | }, 151 | "files": [ 152 | "./config/packages/lexik_jwt_authentication.yaml" 153 | ] 154 | }, 155 | "monolog/monolog": { 156 | "version": "1.24.0" 157 | }, 158 | "namshi/jose": { 159 | "version": "7.2.3" 160 | }, 161 | "nelmio/cors-bundle": { 162 | "version": "1.5", 163 | "recipe": { 164 | "repo": "github.com/symfony/recipes", 165 | "branch": "master", 166 | "version": "1.5", 167 | "ref": "f0436fc35fca88eada758311f8de43bfb61f1980" 168 | }, 169 | "files": [ 170 | "./config/packages/nelmio_cors.yaml" 171 | ] 172 | }, 173 | "nikic/php-parser": { 174 | "version": "v4.2.1" 175 | }, 176 | "ocramius/package-versions": { 177 | "version": "1.4.0" 178 | }, 179 | "ocramius/proxy-manager": { 180 | "version": "2.1.1" 181 | }, 182 | "phpdocumentor/reflection-common": { 183 | "version": "1.0.1" 184 | }, 185 | "phpdocumentor/reflection-docblock": { 186 | "version": "4.3.0" 187 | }, 188 | "phpdocumentor/type-resolver": { 189 | "version": "0.4.0" 190 | }, 191 | "psr/cache": { 192 | "version": "1.0.1" 193 | }, 194 | "psr/container": { 195 | "version": "1.0.0" 196 | }, 197 | "psr/link": { 198 | "version": "1.0.0" 199 | }, 200 | "psr/log": { 201 | "version": "1.1.0" 202 | }, 203 | "psr/simple-cache": { 204 | "version": "1.0.1" 205 | }, 206 | "sensio/framework-extra-bundle": { 207 | "version": "5.2", 208 | "recipe": { 209 | "repo": "github.com/symfony/recipes", 210 | "branch": "master", 211 | "version": "5.2", 212 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" 213 | }, 214 | "files": [ 215 | "./config/packages/sensio_framework_extra.yaml" 216 | ] 217 | }, 218 | "swiftmailer/swiftmailer": { 219 | "version": "v6.2.0" 220 | }, 221 | "symfony/apache-pack": { 222 | "version": "1.0", 223 | "recipe": { 224 | "repo": "github.com/symfony/recipes-contrib", 225 | "branch": "master", 226 | "version": "1.0", 227 | "ref": "c82bead70f9a4f656354a193df7bf0ca2114efa0" 228 | }, 229 | "files": [ 230 | "./public/.htaccess" 231 | ] 232 | }, 233 | "symfony/asset": { 234 | "version": "v4.2.7" 235 | }, 236 | "symfony/browser-kit": { 237 | "version": "v4.2.7" 238 | }, 239 | "symfony/cache": { 240 | "version": "v4.2.7" 241 | }, 242 | "symfony/config": { 243 | "version": "v4.2.7" 244 | }, 245 | "symfony/console": { 246 | "version": "3.3", 247 | "recipe": { 248 | "repo": "github.com/symfony/recipes", 249 | "branch": "master", 250 | "version": "3.3", 251 | "ref": "482d233eb8de91ebd042992077bbd5838858890c" 252 | }, 253 | "files": [ 254 | "./bin/console", 255 | "./config/bootstrap.php" 256 | ] 257 | }, 258 | "symfony/contracts": { 259 | "version": "v1.0.2" 260 | }, 261 | "symfony/css-selector": { 262 | "version": "v4.2.7" 263 | }, 264 | "symfony/debug": { 265 | "version": "v4.2.7" 266 | }, 267 | "symfony/debug-bundle": { 268 | "version": "4.1", 269 | "recipe": { 270 | "repo": "github.com/symfony/recipes", 271 | "branch": "master", 272 | "version": "4.1", 273 | "ref": "f8863cbad2f2e58c4b65fa1eac892ab189971bea" 274 | }, 275 | "files": [ 276 | "./config/packages/dev/debug.yaml" 277 | ] 278 | }, 279 | "symfony/debug-pack": { 280 | "version": "v1.0.7" 281 | }, 282 | "symfony/dependency-injection": { 283 | "version": "v4.2.7" 284 | }, 285 | "symfony/doctrine-bridge": { 286 | "version": "v4.2.7" 287 | }, 288 | "symfony/dom-crawler": { 289 | "version": "v4.2.7" 290 | }, 291 | "symfony/dotenv": { 292 | "version": "v4.2.7" 293 | }, 294 | "symfony/event-dispatcher": { 295 | "version": "v4.2.7" 296 | }, 297 | "symfony/expression-language": { 298 | "version": "v4.2.7" 299 | }, 300 | "symfony/filesystem": { 301 | "version": "v4.2.7" 302 | }, 303 | "symfony/finder": { 304 | "version": "v4.2.7" 305 | }, 306 | "symfony/flex": { 307 | "version": "1.0", 308 | "recipe": { 309 | "repo": "github.com/symfony/recipes", 310 | "branch": "master", 311 | "version": "1.0", 312 | "ref": "dc3fc2e0334a4137c47cfd5a3ececc601fa61a0b" 313 | }, 314 | "files": [ 315 | "./.env" 316 | ] 317 | }, 318 | "symfony/form": { 319 | "version": "v4.2.7" 320 | }, 321 | "symfony/framework-bundle": { 322 | "version": "4.2", 323 | "recipe": { 324 | "repo": "github.com/symfony/recipes", 325 | "branch": "master", 326 | "version": "4.2", 327 | "ref": "f64037a414de7d861f68e9b5b5c0e4f7425e2002" 328 | }, 329 | "files": [ 330 | "./config/bootstrap.php", 331 | "./config/packages/cache.yaml", 332 | "./config/packages/framework.yaml", 333 | "./config/packages/test/framework.yaml", 334 | "./config/services.yaml", 335 | "./public/index.php", 336 | "./src/Controller/.gitignore", 337 | "./src/Kernel.php" 338 | ] 339 | }, 340 | "symfony/http-client": { 341 | "version": "v4.3.0" 342 | }, 343 | "symfony/http-foundation": { 344 | "version": "v4.2.7" 345 | }, 346 | "symfony/http-kernel": { 347 | "version": "v4.2.7" 348 | }, 349 | "symfony/inflector": { 350 | "version": "v4.2.7" 351 | }, 352 | "symfony/intl": { 353 | "version": "v4.2.7" 354 | }, 355 | "symfony/maker-bundle": { 356 | "version": "1.0", 357 | "recipe": { 358 | "repo": "github.com/symfony/recipes", 359 | "branch": "master", 360 | "version": "1.0", 361 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 362 | } 363 | }, 364 | "symfony/monolog-bridge": { 365 | "version": "v4.2.7" 366 | }, 367 | "symfony/monolog-bundle": { 368 | "version": "3.1", 369 | "recipe": { 370 | "repo": "github.com/symfony/recipes", 371 | "branch": "master", 372 | "version": "3.1", 373 | "ref": "18ebf5a940573a20de06f9c4060101eeb438cf3d" 374 | }, 375 | "files": [ 376 | "./config/packages/dev/monolog.yaml", 377 | "./config/packages/prod/monolog.yaml", 378 | "./config/packages/test/monolog.yaml" 379 | ] 380 | }, 381 | "symfony/options-resolver": { 382 | "version": "v4.2.7" 383 | }, 384 | "symfony/orm-pack": { 385 | "version": "v1.0.6" 386 | }, 387 | "symfony/panther": { 388 | "version": "v0.3.0" 389 | }, 390 | "symfony/phpunit-bridge": { 391 | "version": "4.1", 392 | "recipe": { 393 | "repo": "github.com/symfony/recipes", 394 | "branch": "master", 395 | "version": "4.1", 396 | "ref": "0e548dd90adba18fabd4ef419b14d361fe4d6c74" 397 | }, 398 | "files": [ 399 | "./.env.test", 400 | "./bin/phpunit", 401 | "./config/bootstrap.php", 402 | "./phpunit.xml.dist", 403 | "./tests/.gitignore" 404 | ] 405 | }, 406 | "symfony/polyfill-intl-icu": { 407 | "version": "v1.11.0" 408 | }, 409 | "symfony/polyfill-intl-idn": { 410 | "version": "v1.11.0" 411 | }, 412 | "symfony/polyfill-mbstring": { 413 | "version": "v1.11.0" 414 | }, 415 | "symfony/polyfill-php72": { 416 | "version": "v1.11.0" 417 | }, 418 | "symfony/polyfill-php73": { 419 | "version": "v1.11.0" 420 | }, 421 | "symfony/process": { 422 | "version": "v4.2.7" 423 | }, 424 | "symfony/profiler-pack": { 425 | "version": "v1.0.4" 426 | }, 427 | "symfony/property-access": { 428 | "version": "v4.2.7" 429 | }, 430 | "symfony/property-info": { 431 | "version": "v4.2.7" 432 | }, 433 | "symfony/routing": { 434 | "version": "4.2", 435 | "recipe": { 436 | "repo": "github.com/symfony/recipes", 437 | "branch": "master", 438 | "version": "4.2", 439 | "ref": "5374e24d508ba8fd6ba9eb15170255fdb778316a" 440 | }, 441 | "files": [ 442 | "./config/packages/dev/routing.yaml", 443 | "./config/packages/routing.yaml", 444 | "./config/packages/test/routing.yaml", 445 | "./config/routes.yaml" 446 | ] 447 | }, 448 | "symfony/security-bundle": { 449 | "version": "3.3", 450 | "recipe": { 451 | "repo": "github.com/symfony/recipes", 452 | "branch": "master", 453 | "version": "3.3", 454 | "ref": "f8a63faa0d9521526499c0a8f403c9964ecb0527" 455 | }, 456 | "files": [ 457 | "./config/packages/security.yaml" 458 | ] 459 | }, 460 | "symfony/security-core": { 461 | "version": "v4.2.7" 462 | }, 463 | "symfony/security-csrf": { 464 | "version": "v4.2.7" 465 | }, 466 | "symfony/security-guard": { 467 | "version": "v4.2.7" 468 | }, 469 | "symfony/security-http": { 470 | "version": "v4.2.7" 471 | }, 472 | "symfony/serializer": { 473 | "version": "v4.2.7" 474 | }, 475 | "symfony/serializer-pack": { 476 | "version": "v1.0.2" 477 | }, 478 | "symfony/stopwatch": { 479 | "version": "v4.2.7" 480 | }, 481 | "symfony/swiftmailer-bundle": { 482 | "version": "2.5", 483 | "recipe": { 484 | "repo": "github.com/symfony/recipes", 485 | "branch": "master", 486 | "version": "2.5", 487 | "ref": "3db029c03e452b4a23f7fc45cec7c922c2247eb8" 488 | }, 489 | "files": [ 490 | "./config/packages/dev/swiftmailer.yaml", 491 | "./config/packages/swiftmailer.yaml", 492 | "./config/packages/test/swiftmailer.yaml" 493 | ] 494 | }, 495 | "symfony/test-pack": { 496 | "version": "v1.0.5" 497 | }, 498 | "symfony/translation": { 499 | "version": "3.3", 500 | "recipe": { 501 | "repo": "github.com/symfony/recipes", 502 | "branch": "master", 503 | "version": "3.3", 504 | "ref": "1fb02a6e1c8f3d4232cce485c9afa868d63b115a" 505 | }, 506 | "files": [ 507 | "./config/packages/translation.yaml", 508 | "./translations/.gitignore" 509 | ] 510 | }, 511 | "symfony/twig-bridge": { 512 | "version": "v4.2.7" 513 | }, 514 | "symfony/twig-bundle": { 515 | "version": "3.3", 516 | "recipe": { 517 | "repo": "github.com/symfony/recipes", 518 | "branch": "master", 519 | "version": "3.3", 520 | "ref": "369b5b29dc52b2c190002825ae7ec24ab6f962dd" 521 | }, 522 | "files": [ 523 | "./config/packages/twig.yaml", 524 | "./config/routes/dev/twig.yaml", 525 | "./templates/base.html.twig" 526 | ] 527 | }, 528 | "symfony/validator": { 529 | "version": "4.1", 530 | "recipe": { 531 | "repo": "github.com/symfony/recipes", 532 | "branch": "master", 533 | "version": "4.1", 534 | "ref": "9a285e4ff3915c7cd086e9945f30591a926baf83" 535 | }, 536 | "files": [ 537 | "./config/packages/test/validator.yaml", 538 | "./config/packages/validator.yaml" 539 | ] 540 | }, 541 | "symfony/var-dumper": { 542 | "version": "v4.2.7" 543 | }, 544 | "symfony/var-exporter": { 545 | "version": "v4.2.7" 546 | }, 547 | "symfony/web-link": { 548 | "version": "v4.2.7" 549 | }, 550 | "symfony/web-profiler-bundle": { 551 | "version": "3.3", 552 | "recipe": { 553 | "repo": "github.com/symfony/recipes", 554 | "branch": "master", 555 | "version": "3.3", 556 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6" 557 | }, 558 | "files": [ 559 | "./config/packages/dev/web_profiler.yaml", 560 | "./config/packages/test/web_profiler.yaml", 561 | "./config/routes/dev/web_profiler.yaml" 562 | ] 563 | }, 564 | "symfony/web-server-bundle": { 565 | "version": "3.3", 566 | "recipe": { 567 | "repo": "github.com/symfony/recipes", 568 | "branch": "master", 569 | "version": "3.3", 570 | "ref": "dae9b39fd6717970be7601101ce5aa960bf53d9a" 571 | } 572 | }, 573 | "symfony/webpack-encore-bundle": { 574 | "version": "1.0", 575 | "recipe": { 576 | "repo": "github.com/symfony/recipes", 577 | "branch": "master", 578 | "version": "1.0", 579 | "ref": "7b6180725839c090cd8dbd069a3947b0dca1bae0" 580 | }, 581 | "files": [ 582 | "./assets/css/app.css", 583 | "./assets/js/app.js", 584 | "./config/packages/assets.yaml", 585 | "./config/packages/prod/webpack_encore.yaml", 586 | "./config/packages/webpack_encore.yaml", 587 | "./package.json", 588 | "./webpack.config.js" 589 | ] 590 | }, 591 | "symfony/yaml": { 592 | "version": "v4.2.7" 593 | }, 594 | "twig/twig": { 595 | "version": "v2.8.1" 596 | }, 597 | "webmozart/assert": { 598 | "version": "1.4.0" 599 | }, 600 | "willdurand/negotiation": { 601 | "version": "v2.3.1" 602 | }, 603 | "zendframework/zend-code": { 604 | "version": "3.3.1" 605 | }, 606 | "zendframework/zend-eventmanager": { 607 | "version": "3.2.1" 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /templates/app/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title "Mon application React" %} 4 | 5 | {% block stylesheets %} 6 | 7 | 8 | {{ encore_entry_link_tags('app') }} 9 | {% endblock %} 10 | 11 | {% block body %} 12 |
13 | {% endblock %} 14 | 15 | {% block javascripts %} 16 | {{ encore_entry_script_tags('app') }} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Welcome! 7 | {% endblock %} 8 | 9 | {% block stylesheets %}{% endblock %} 10 | 11 | 12 | {% block body %}{% endblock %} 13 | 14 | {% block javascripts %}{% endblock %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | console.log(process.env); 4 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liorchamla/formation-api-platform-react/599aeff7496c6c3a573501a36c961b1f05a62338/tests/.gitignore -------------------------------------------------------------------------------- /translations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liorchamla/formation-api-platform-react/599aeff7496c6c3a573501a36c961b1f05a62338/translations/.gitignore -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | var Encore = require("@symfony/webpack-encore"); 4 | 5 | Encore 6 | // directory where compiled assets will be stored 7 | .setOutputPath("public/build/") 8 | // public path used by the web server to access the output path 9 | .setPublicPath("/build") 10 | // only needed for CDN's or sub-directory deploy 11 | //.setManifestKeyPrefix('build/') 12 | 13 | /* 14 | * ENTRY CONFIG 15 | * 16 | * Add 1 entry for each "page" of your app 17 | * (including one that's included on every page - e.g. "app") 18 | * 19 | * Each entry will result in one JavaScript file (e.g. app.js) 20 | * and one CSS file (e.g. app.css) if you JavaScript imports CSS. 21 | */ 22 | .addEntry("app", "./assets/js/app.js") 23 | //.addEntry('page1', './assets/js/page1.js') 24 | //.addEntry('page2', './assets/js/page2.js') 25 | 26 | // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. 27 | .splitEntryChunks() 28 | 29 | // will require an extra script tag for runtime.js 30 | // but, you probably want this, unless you're building a single-page app 31 | .enableSingleRuntimeChunk() 32 | 33 | /* 34 | * FEATURE CONFIG 35 | * 36 | * Enable & configure other features below. For a full 37 | * list of features, see: 38 | * https://symfony.com/doc/current/frontend.html#adding-more-features 39 | */ 40 | .cleanupOutputBeforeBuild() 41 | .enableBuildNotifications() 42 | .enableSourceMaps(!Encore.isProduction()) 43 | // enables hashed filenames (e.g. app.abc123.css) 44 | .enableVersioning(Encore.isProduction()) 45 | 46 | // enables @babel/preset-env polyfills 47 | .configureBabel(() => {}, { 48 | useBuiltIns: "usage", 49 | corejs: 3 50 | }) 51 | 52 | // enables Sass/SCSS support 53 | //.enableSassLoader() 54 | 55 | // uncomment if you use TypeScript 56 | //.enableTypeScriptLoader() 57 | // uncomment to get integrity="..." attributes on your script & link tags 58 | // requires WebpackEncoreBundle 1.4 or higher 59 | //.enableIntegrityHashes() 60 | 61 | // uncomment if you're having problems with a jQuery plugin 62 | //.autoProvidejQuery() 63 | 64 | // uncomment if you use API Platform Admin (composer req api-admin) 65 | .enableReactPreset(); 66 | //.addEntry('admin', './assets/js/admin.js') 67 | 68 | Encore.configureDefinePlugin(options => { 69 | options["process.env"].API_URL = process.env.API_URL; 70 | }); 71 | 72 | module.exports = Encore.getWebpackConfig(); 73 | --------------------------------------------------------------------------------