├── .gitignore
├── src
├── constants.js
├── history.js
├── components
│ ├── about
│ │ └── AboutPage.js
│ ├── admin
│ │ └── AdminPage.js
│ ├── home
│ │ └── HomePage.js
│ ├── App.js
│ └── Header.js
├── css
│ └── app.css
├── main.js
└── svg
│ └── icons.svg
├── public
├── index.php
└── api
│ └── api.php
├── webpack-production.config.js
├── package.json
├── webpack.config.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | public/build
3 | /nbproject/
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * Define any constants that need to be used application-wide
5 | */
6 |
7 | // uri for API calls
8 | export var apiUrl = window.location.origin + '/api/api.php';
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/history.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * Put the browser history in its own module so it can be accessed anywhere in the
5 | * application.
6 | */
7 |
8 | import createHashHistory from 'history/lib/createHashHistory'
9 |
10 | export default createHashHistory({
11 | // hide the hash key (e.g. "_k=123abc")
12 | queryKey: false
13 | });
--------------------------------------------------------------------------------
/src/components/about/AboutPage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 |
5 | /**
6 | * About Page
7 | */
8 | var AboutPage = React.createClass({
9 | componentDidMount: function() {
10 | console.log('AboutPage.js');
11 | },
12 | render: function() {
13 | return (
14 |
15 |
About
16 |
17 | );
18 | }
19 | });
20 |
21 | export default AboutPage;
--------------------------------------------------------------------------------
/src/components/admin/AdminPage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 |
5 | /**
6 | * Admin Page
7 | */
8 | var AdminPage = React.createClass({
9 | componentDidMount: function() {
10 | console.log('AdminPage.js');
11 | },
12 | render: function() {
13 | return (
14 |
15 |
Admin
16 |
17 | );
18 | }
19 | });
20 |
21 | export default AdminPage;
--------------------------------------------------------------------------------
/webpack-production.config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var webpack = require('webpack');
4 | var webpackConfig = require('./webpack.config.js');
5 |
6 | // strip out console.log statements
7 | webpackConfig.module.loaders.push({
8 | test: /\.js$/,
9 | exclude: /node_modules/,
10 | loader: 'strip?strip[]=console.log!babel'
11 | });
12 |
13 | // set node env to production
14 | webpackConfig.plugins.push(
15 | new webpack.DefinePlugin({
16 | 'process.env.NODE_ENV': JSON.stringify('production')
17 | })
18 | );
19 |
20 | module.exports = webpackConfig;
--------------------------------------------------------------------------------
/src/css/app.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Bootstrap overrides
3 | */
4 | .navbar { border-radius: 0; }
5 | .navbar-inverse .navbar-nav > li > .active,
6 | .navbar-inverse .navbar-brand.active { color: #fff; }
7 |
8 | /* Icons as SVG element */
9 | .icon {
10 | display: inline-block;
11 | height: 1em;
12 | vertical-align: middle;
13 | width: 1em;
14 | }
15 | .icon-small { height: 1em; width: 1em; }
16 | .icon-medium { height: 2em; width: 2em; }
17 | .icon-large { height: 3em; width: 3em; }
18 | .navbar-brand .icon { margin-right: .5em; vertical-align: top; }
19 |
--------------------------------------------------------------------------------
/src/components/home/HomePage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import rp from 'request-promise';
5 | import { apiUrl } from '../../../src/constants.js';
6 |
7 | /**
8 | * Home Page
9 | */
10 | var HomePage = React.createClass({
11 | componentDidMount: function() {
12 | console.log('HomePage.js');
13 | },
14 | handleClick: function() {
15 | rp({
16 | uri: apiUrl,
17 | json: true,
18 | qs: { action: 'hello' }
19 | })
20 | .then(function(response) {
21 | console.log(response);
22 | })
23 | .catch(function(error) {
24 | console.log(error);
25 | });
26 | },
27 | render: function() {
28 | return (
29 |
30 |
Home
31 |
32 |
33 | );
34 | }
35 | });
36 |
37 | export default HomePage;
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import Header from './Header.js';
5 | import {} from 'bootstrap/dist/css/bootstrap.css';
6 | import {} from '../css/app.css';
7 | import {} from '../svg/icons.svg';
8 |
9 | /**
10 | * Application component
11 | *
12 | * This is the parent component for all routes in the application. It displays
13 | * the header and wraps the content of the current route in a container div.
14 | */
15 | var App = React.createClass({
16 | componentDidMount: function() {
17 | console.log('App.js');
18 | },
19 | render: function() {
20 | return (
21 |
22 |
23 |
24 |
25 | {this.props.children}
26 |
27 |
28 |
29 | );
30 | }
31 | });
32 |
33 | export default App;
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import { Link, IndexLink } from 'react-router';
5 |
6 | var activeClassName = 'active';
7 |
8 | /**
9 | * Header navigation bar
10 | */
11 | var Header = React.createClass({
12 | componentDidMount: function() {
13 | console.log('Header.js');
14 | },
15 | render: function() {
16 | return (
17 |
41 | );
42 | }
43 | });
44 |
45 | export default Header;
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * This is the entry point for the webpack bundle.
5 | *
6 | * Import any libraries needed across the whole application. Define navigation
7 | * routes and import the top-level component for each route.
8 | */
9 |
10 | import React from 'react';
11 | import { render } from 'react-dom';
12 | import { Router, Route, IndexRoute, Redirect } from 'react-router';
13 | import rp from 'request-promise';
14 | import history from './history.js';
15 | import App from './components/App.js';
16 | import HomePage from './components/home/HomePage.js';
17 | import AboutPage from './components/about/AboutPage.js';
18 | import AdminPage from './components/admin/AdminPage.js';
19 | import { apiUrl } from './constants.js';
20 |
21 | console.log('main.js');
22 |
23 | function isAdmin() {
24 | return rp({
25 | uri: apiUrl,
26 | json: true,
27 | qs: { action: 'isAdmin' }
28 | }).then(function(isAdmin) {
29 | return isAdmin;
30 | }).catch(function(error) {
31 | console.log(error);
32 | });
33 | }
34 |
35 | function requireAdmin(transition) {
36 | console.log(transition);
37 | isAdmin().then(function(isAdmin) {
38 | // redirect if not admin
39 | if (!isAdmin) {
40 | history.replaceState(null, '/');
41 | }
42 | });
43 | }
44 |
45 | render((
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ), document.getElementById('app'));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-webpack-php-starter",
3 | "version": "1.0.0",
4 | "description": "A boilerplate starter project for a React application using webpack with a PHP back-end.",
5 | "main": "webpack.config.js",
6 | "scripts": {
7 | "start": "webpack --watch",
8 | "build": "NODE_ENV=production webpack -p --config ./webpack-production.config.js",
9 | "prebuild": "rimraf public/build"
10 | },
11 | "author": "Brett Rawlins",
12 | "license": "ISC",
13 | "dependencies": {
14 | "react": "^15.3.2",
15 | "react-dom": "^15.3.2",
16 | "react-router": "^3.0.0",
17 | "webpack": "^1.13.3"
18 | },
19 | "devDependencies": {
20 | "babel-core": "^6.18.0",
21 | "babel-loader": "^6.2.7",
22 | "babel-preset-es2015": "^6.18.0",
23 | "babel-preset-react": "^6.16.0",
24 | "bluebird": "^3.4.6",
25 | "bootstrap": "^3.3.7",
26 | "browser-sync": "^2.17.5",
27 | "browser-sync-webpack-plugin": "^1.1.3",
28 | "cls-bluebird": "^2.0.1",
29 | "continuation-local-storage": "^3.2.0",
30 | "css-loader": "^0.25.0",
31 | "exports-loader": "^0.6.3",
32 | "expose-loader": "^0.7.1",
33 | "extract-text-webpack-plugin": "^1.0.1",
34 | "file-loader": "^0.9.0",
35 | "fs": "0.0.1-security",
36 | "history": "^3.2.0",
37 | "imports-loader": "^0.6.5",
38 | "jquery": "^3.1.1",
39 | "json-loader": "^0.5.4",
40 | "less": "^2.7.1",
41 | "less-loader": "^2.2.3",
42 | "lodash": "^4.16.6",
43 | "path": "^0.12.7",
44 | "request-promise": "^4.1.1",
45 | "rimraf": "^2.5.4",
46 | "strip-loader": "^0.1.2",
47 | "style-loader": "^0.13.1",
48 | "url-loader": "^0.5.7"
49 | },
50 | "babel": {
51 | "presets": [
52 | "react",
53 | "es2015"
54 | ]
55 | }
56 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path');
4 | var webpack = require('webpack');
5 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
6 | var BrowserSyncPlugin = require('browser-sync-webpack-plugin');
7 |
8 | module.exports = {
9 | entry: [
10 | path.join(__dirname, 'src/main.js')
11 | ],
12 | output: {
13 | path: path.join(__dirname, 'public/build/'),
14 | filename: 'bundle.js'
15 | },
16 | plugins: [
17 | // output a separate css bundle
18 | new ExtractTextPlugin('bundle.css'),
19 |
20 | // reloads browser when the watched files change
21 | new BrowserSyncPlugin({
22 | // use existing Apache virtual host
23 | proxy: 'http://local.react-starter/',
24 | tunnel: true,
25 | // watch the built files and the index file
26 | files: ['public/build/*', 'public/index.php']
27 | }),
28 |
29 | // set node env
30 | new webpack.DefinePlugin({
31 | 'process.env.NODE_ENV': JSON.stringify('development')
32 | })
33 | ],
34 | module: {
35 | loaders: [
36 | // transpiles JSX and ES6
37 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' },
38 |
39 | // makes jQuery available to Bootstrap js
40 | { test: /bootstrap\/js\//, loader: 'imports?jQuery=jquery' },
41 |
42 | // extracts css as separate output file
43 | { test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css') },
44 |
45 | // loads font icons for Bootstrap css
46 | { test: /\.woff(2?)(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff" },
47 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream" },
48 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" },
49 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml" },
50 |
51 | { test: /\.json$/, loader: 'json' }
52 | ]
53 | },
54 | // needed to make request-promise work
55 | node: {
56 | fs: 'empty',
57 | net: 'empty',
58 | tls: 'empty'
59 | }
60 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-webpack-php-starter
2 |
3 | This is a boilerplate React application that I use to start new projects. It uses webpack to build the JavaScript bundle file and PHP for the back-end API. Inspired by [Christian Alfoni's ultimate webpack setup](http://www.christianalfoni.com/articles/2015_04_19_The-ultimate-webpack-setup).
4 |
5 | ## Overview
6 |
7 | React apps are often served using a node server like Express. However, in my case I wanted to use Apache because we use PHP to manage user authentication. I know there are packages that theoretically can make Express serve PHP, but it was easier to just use our existing Apache server.
8 |
9 | This happens to be my specific use case at the moment, but this project could easily be adapted to use a node server instead.
10 |
11 | ## Goals
12 |
13 | I wanted a setup that would enable the following features:
14 |
15 | * Bundling and minification using webpack
16 | * ECMAScript 2015 (ES6) syntax via Babel
17 | * Linting
18 | * Separate builds for development and production
19 | * Bootstrap styles
20 | * SVG icons
21 | * PHP index file and back-end API
22 | * Automatic browser reloading using Browsersync
23 |
24 | ## Directory Structure
25 |
26 | The project is structured like this:
27 |
28 | * /public (web root)
29 | - /api (back-end API in PHP)
30 | - /build (built code)
31 | - index.php
32 | * /src (source code that needs to go through the build process)
33 | - /components (React components)
34 | - /css
35 | - /svg
36 | - constants.js (application-wide variables)
37 | - history.js (browser history as separate module)
38 | - main.js (the webpack entry file)
39 | * .gitignore
40 | * package.json
41 | * README.md
42 | * webpack-production.config.js
43 | * webpack-config.js
44 |
45 | The `public` directory is the web root. Configure the server to serve files from there. The `build` directory is where the webpack output files go. This directory is ignored by git.
46 |
47 | ## Builds
48 |
49 | The "scripts" section of the `package.json` file contains the build scripts. When you first checkout the project you'll need to run `npm install` to get all the node modules used.
50 |
51 | For development type:
52 |
53 | npm start
54 |
55 | The development build runs webpack in watch mode. Whenever you save changes to a file in the `src` folder it rebuilds the bundle.
56 |
57 | For production type:
58 |
59 | npm run build
60 |
61 | The production build deletes the `build` folder and rebuilds the bundle using the settings in `webpack-production.config.js`. Code gets minified and console logs get stripped out.
62 |
--------------------------------------------------------------------------------
/src/svg/icons.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/api/api.php:
--------------------------------------------------------------------------------
1 | _processRequest();
15 | }
16 |
17 | /**
18 | * Routes incoming requests to the corresponding method
19 | *
20 | * Converts $_REQUEST to an object, then checks for the given action and
21 | * calls that method. All the request parameters are stored under
22 | * $this->request.
23 | */
24 | private function _processRequest()
25 | {
26 | // prevent unauthenticated access to API
27 | $this->_secureBackend();
28 |
29 | // get the request
30 | if (!empty($_REQUEST)) {
31 | // convert to object for consistency
32 | $this->request = json_decode(json_encode($_REQUEST));
33 | } else {
34 | // already object
35 | $this->request = json_decode(file_get_contents('php://input'));
36 | }
37 |
38 | // get the action
39 | $action = $this->request->action;
40 |
41 | if (empty($action)) {
42 | $message = array('error' => 'No method given.');
43 | $this->reply($message, 400);
44 | } else {
45 | // call the corresponding method
46 | if (method_exists($this, $action)) {
47 | $this->$action();
48 | } else {
49 | $message = array('error' => 'Method not found.');
50 | $this->reply($message, 400);
51 | }
52 | }
53 | }
54 |
55 | /**
56 | * Prevent unauthenticated access to the backend
57 | */
58 | private function _secureBackend()
59 | {
60 | if (!$this->_isAuthenticated()) {
61 | header("HTTP/1.1 401 Unauthorized");
62 | exit();
63 | }
64 | }
65 |
66 | /**
67 | * Check if user is authenticated
68 | *
69 | * This is just a placeholder. Here you would check the session or similar
70 | * to see if the user is logged in and/or authorized to make API calls.
71 | */
72 | private function _isAuthenticated()
73 | {
74 | return true;
75 | }
76 |
77 | /**
78 | * Returns JSON data with HTTP status code
79 | *
80 | * @param array $data - data to return
81 | * @param int $status - HTTP status code
82 | * @return JSON
83 | */
84 | private function reply($data, $status = 200)
85 | {
86 | $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1');
87 | header($protocol . ' ' . $status);
88 | header('Content-Type: application/json');
89 | echo json_encode($data);
90 | exit;
91 | }
92 |
93 | public function hello()
94 | {
95 | $this->reply('Hello from the API!');
96 | }
97 |
98 | /**
99 | * Determines if the logged in user has admin rights
100 | *
101 | * This is just a placeholder. Here you would check the session or database
102 | * to see if the user has admin rights.
103 | *
104 | * @return boolean
105 | */
106 | public function isAdmin()
107 | {
108 | $this->reply(true);
109 | }
110 |
111 | }
112 |
113 | $MyApi = new MyApi();
--------------------------------------------------------------------------------