├── .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 | 2 | 3 | 4 | loader 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 24 | 29 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /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(); --------------------------------------------------------------------------------