├── .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 |
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 | Id. |
94 | Client |
95 | Email |
96 | Entreprise |
97 | Factures |
98 | Montant total |
99 | |
100 |
101 |
102 |
103 | {!loading && (
104 |
105 | {paginatedCustomers.map(customer => (
106 |
107 | {customer.id} |
108 |
109 |
110 | {customer.firstName} {customer.lastName}
111 |
112 | |
113 | {customer.email} |
114 | {customer.company} |
115 |
116 |
117 | {customer.invoices.length}
118 |
119 | |
120 |
121 | {customer.totalAmount.toLocaleString()} €
122 | |
123 |
124 |
131 | |
132 |
133 | ))}
134 |
135 | )}
136 |
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 | Id. |
60 | Client |
61 | Email |
62 | Entreprise |
63 | Factures |
64 | Montant total |
65 | |
66 |
67 |
68 |
69 |
70 | {loading && (
71 |
72 | Chargement ... |
73 |
74 | )}
75 | {!loading &&
76 | customers.map(customer => (
77 |
78 | {customer.id} |
79 |
80 |
81 | {customer.firstName} {customer.lastName}
82 |
83 | |
84 | {customer.email} |
85 | {customer.company} |
86 |
87 |
88 | {customer.invoices.length}
89 |
90 | |
91 |
92 | {customer.totalAmount.toLocaleString()} €
93 | |
94 |
95 |
102 | |
103 |
104 | ))}
105 |
106 |
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 |
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 | Numéro |
110 | Client |
111 | Date d'envoi |
112 | Statut |
113 | Montant |
114 | |
115 |
116 |
117 | {!loading && (
118 |
119 | {paginatedInvoices.map(invoice => (
120 |
121 | {invoice.chrono} |
122 |
123 |
124 | {invoice.customer.firstName} {invoice.customer.lastName}
125 |
126 | |
127 | {formatDate(invoice.sentAt)} |
128 |
129 |
132 | {STATUS_LABELS[invoice.status]}
133 |
134 | |
135 |
136 | {invoice.amount.toLocaleString()} €
137 | |
138 |
139 |
143 | Editer
144 |
145 |
151 | |
152 |
153 | ))}
154 |
155 | )}
156 |
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 |
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 |
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 |
--------------------------------------------------------------------------------