Вы наверняка думаете, что писать на php — это просто. И «hello, world» выглядит примерно так так: <?php echo 'Hello, world!';
Конечно, чего еще ожидать от языка с низким порогом входа. Ну да, именно так и было раньше. Много лет назад. Но теперь, в 2017 году никто так уже не делает. Давайте рассмотрим, почему, и попробуем построить наше более реалистичное hello-world приложение по шагам, а их, скажу сразу, получилось не мало.
→ Полный исходный код «hello,world» можно посмотреть здесь.
Для начала надо осознать тот факт, что без фреймворка сейчас приложения никто не делает. Если вы пишете вручную "
echo 'hello, world'", то обрекаете проект на говнокод на веки вечные (кто потом этот велосипед за вас переписывать будет?). Поэтому возьмем какой-нибудь современный, распространенный в мире фреймворк, например Symfony.Но прежде, чем его устанавливать, надо бы создать базу данных. Зачем базу данных? Ну не хардкодить же строку «hello, world» прямо в тексте программы!
База данных
В 2017 году принято использовать postgresql. Если вы вдруг еще не умеете его устанавливать, я помогу:
sudo apt-get install postgresql
Убунта при установке создаст юзера postgres, из под которого можно запустить команду psql с полными правами на базу.
sudo -u postgres psql
Теперь создадим юзера базы с паролем (придумайте какой-нибудь посложнее).
CREATE ROLE helloworlduser WITH PASSWORD '12345' LOGIN;
И саму базу:
CREATE DATABASE helloworld OWNER helloworlduser;
Также надо убедиться, что в pg_hba.conf у вас разрешены коннекты к базе с localhost (127.0.0.1). Там должно быть что-то вроде этого:
host all all 127.0.0.1/32 md5
Проверим соединение:
psql -h localhost -U helloworlduser helloworld
после ввода пароля должно пустить в базу. Сразу создадим таблицу:
CREATE TABLE greetings ( id int, greeting text, primary key(id) ); INSERT INTO greetings (id, greeting) VALUES (1, 'Hello, world!');
Ну, супер, с базой всё. Теперь перейдем к фреймворку
php-фреймворк
Надеюсь, что в 2017 году у всех стоит composer на компьютере. Поэтому сразу перейдем к установке фреймворка
composer create-project symfony/framework-standard-edition helloworldphp
При установке он сразу спросит параметры соединения с базой:
host: 127.0.0.1 database_name: helloworld database_user: helloworlduser database_password: 12345
остальное по умолчанию/по усмотрению.
Надо только в конфиге config.yml поменть драйвер на
driver: pdo_pgsql. (У вас ведь установлено php-расширение pdo_pgsql ?)Проверим, что всё более менее работает, запустив
cd helloworldphp bin/console server:start
Симфони запустит свой собственный сервер, который слушает порт 8000 и на нем можно дебажить код. Таким образом в браузере по адресу
http://localhost:8000/ должно быть что-то вроде «Это симфони, блаблабла».Уфф! Казалось бы всё, контроллер уже есть, подправить вьюху, создать модель и понеслась, хелло ворлд уже близко!
Но… нет. Извините, но не в 2017-ом. В этом году все делают SPA (single page application).
Php-программист в 2017 году не может обойтись без js и верстки, теперь мы все full stack, а значит и helloworld должен быть соответствующий.
Ну ладно, ладно, еще бывают чистые php-бекенд-разработчики, но давайте возьмем более общий случай
JavaScript и его многочисленные друзья
Поэтому находим в симфони вьюху (а дефолтная вьюха лежит в app/Resources/view/default/index.html.twig) и стираем там всё, заменяя на:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <div id="root"></div> <script src="js/bundle.js"></script> </body> </html>
Т.е. всё будет лежат в bundle.js: сжатые javascript файлы прямо вместе со стилями и всем, чем нужно.
Как нам создать этот бандл? Нужно написать приложение и настроить webpack для сборки.
Webpack (или его аналоги) нам все равно бы понадобились, мы же не будем писать код на чистом javascript в 2017-году, когда typescript явно в тренде. А typescript надо как-то преобразовать в обычную js-ку. Это удобно делать, используя webpack.
Разумеется, на чистом typescript тоже никто не пишет. Нужен какой-то фреймворк. Одна из самых модных связок сейчас — это react + redux. А для верстки, так и быть, будем использовать старый добрый олдскульный bootstrap (через sass, конечно же).
Нам понадобится куча js-библиотек. У вас ведь стоит nodejs и npm? Убедитесь, что у вас свежий npm и установите пакеты:
npm init
в зависимостях (в файле package.json) пропишем примерно такое:
"dependencies": { "@types/react": "^15.0.11", "@types/react-dom": "^0.14.23", "babel-core": "^6.23.1", "babel-loader": "^6.3.2", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.23.0", "bootstrap-sass": "^3.3.7", "css-loader": "^0.26.1", "node-sass": "^4.5.0", "react": "^15.4.2", "react-dom": "^15.4.2", "react-redux": "^5.0.2", "redux": "^3.6.0", "resolve-url-loader": "^2.0.0", "sass-loader": "^6.0.1", "style-loader": "^0.13.1", "ts-loader": "^2.0.0", "typescript": "^2.1.6", "url-loader": "^0.5.7", "webpack": "^2.2.1", "@types/node": "^7.0.5" }
И выполним
npm install
и еще нужно установить:
npm install webpack -g
чтобы была доступна команда webpack.
Увы, это еще далеко не всё. Так как у нас typescript, еще надо создать файл tsconfig.json, примерно такой:
tsconfig.json
{ "compilerOptions": { "module": "es6", "moduleResolution": "node", "sourceMap": false, "target": "esnext", "outDir": "web/ts", "lib": [ "dom", "scripthost", "es5", "es6", "es7" ], "jsx": "react" }, "include": [ "frontend/**/*.ts", "frontend/**/*.tsx" ] }
С конфигами пока что ок, теперь займемся нашим приложением на typescript.
Сначала создадим компонент для отображения нашего текста:
// файл frontend/components/Greetings.tsx import * as React from 'react'; export interface GreetingsProps { text: string; isReady: boolean; onMount(); } class Greetings extends React.Component<GreetingsProps, undefined> { componentDidMount() { this.props.onMount(); } render() { return ( <h1>{this.props.text}</h1> ); } } export default Greetings;
Наше SPA будет подгружать текст надписи через Rest API. React — это просто view-компоненты, а нам еще нужна логика приложения и управление состоянием.
Так что будем использовать redux, а также пакет для связи redux и react (react-redux). Поэтому надо будет еще создать компонент, который будет создавать наш компонент Greetings с нужными properties, и сможет сообщить хранилищу (store) состояния, что появилось новое действие (получены данные для отображения).
Disclaimer: я только начал изучать redux, поэтому наверняка тут есть за что «бить по рукам».
Выглядит этот компонент, допустим, примерно так:
// файл frontend/components/App.tsx import * as React from 'react'; import {connect} from 'react-redux' import Greetings from './Greetings'; const mapStateToProps = (state) => { return state; } const mapDispatchToProps = (dispatch) => { return { onMount: () => { fetch("/greetings/1").then((response) => { return response.json(); }).then((json) => { dispatch({type: 'FETCH_GREETING', text: json.greeting}) }); } } } export default connect(mapStateToProps, mapDispatchToProps)(Greetings);
Ну и точка входа приложения, создание redux-стора, диспатчера и т.д. Тут всё сделано немного по рабоче-крестьянски, но для хелловорлда сойдет, пожалуй:
// подгружает стили bootstrap import 'bootstrap-sass/assets/stylesheets/_bootstrap.scss'; import * as React from 'react'; import * as ReactDOM from "react-dom"; import {Provider} from 'react-redux'; import App from './components/App'; import {createStore} from 'redux'; const app = (state = {isReady: false, text: ''}, action) => { switch (action.type) { case 'FETCH_GREETING': return Object.assign({}, state, {isReady: true, text: action.text}); } return state; } const store = createStore(app); ReactDOM.render( <Provider store={store}> <App/> </Provider>, document.getElementById("root") );
Примерно здесь происходит следующее:
- Первоначальное состояние системы —
{isReady: false, text: ''}. - Создан reducer под названием app, который умеет обрабатывать действие FETCH_GREETING и возвращать новое состояние системы.
- Создан store для обработки состояний.
- Всё отрендеривается в элемент, который мы прописали во вьюхе
<div id="root"></div>
Ах да, совсем забыл. Конфиг вебпака:
webpack.config.js
const webpack = require('webpack'); const path = require('path'); const ENVIRONMENT = process.env.NODE_ENV || 'development'; let config = { context: path.resolve(__dirname, "frontend"), entry: './index.tsx', output: { filename: 'bundle.js', path: path.resolve(__dirname, "web/js") }, resolve: { extensions: [ ".js", ".jsx", '.ts', '.tsx'] }, module: { rules: [ { test: /\.tsx?$/, use: [{ loader: 'babel-loader', query: { presets: ['es2015', 'react'] } }, { loader: 'ts-loader' }] }, { test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/, loader: 'url-loader' }, { test: /\.scss$/, use: [ { loader: "style-loader" }, { loader: "css-loader" }, { loader: "resolve-url-loader" }, { loader: "sass-loader" } ] } ] }, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(ENVIRONMENT) }) ], node: { process: false } }; if (ENVIRONMENT == 'production') { config.plugins.push( new webpack.optimize.UglifyJsPlugin({ compress: { drop_console: false, warnings: false } }) ); } module.exports = config;
Теперь мы можем запустить webpack или NODE_ENV=production webpack (чтобы получить минифицированную версию bundle.js)
Pomodoro
Не знаю как вы, а я уже задолбался писать этот hello, world. В 2017 году надо работать эффективно, а это подразумевает, что надо делать перерывы в работе (метод Pomodoro и т.д.). Так что, пожалуй, прервусь не надолго.
[прошло какое-то время]
Давайте продолжим. Мы уже умеем подгружать код с /greetings/1 на стороне javascript, но php-часть еще совершенно не готова.
Doctrine
Уже потрачено много времени, а в php-коде не создано ни одной сущности. Давайте исправим положение:
<?php // src/AppBundle/Entity/Greeting.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="greetings") */ class Greeting { /** * @ORM\Column(type="integer") * @ORM\Id */ private $id; /** * @ORM\Column(type="string", length=100) */ private $greeting; public function getId() { return $this->id; } public function getGreeting() { return $this->greeting; } }
Супер. Осталось совсем чуть-чуть.
REST
Надо сделать-таки простенький REST API, который может хотя бы отдать json по запросу GET /greetings/1
Для этого в контроллере (файл src/AppBundle/Controller/DefaultController.php) добавим метод с роутом:
/** * @Route("/greetings/{id}") */ public function greetings($id) { $greeting = $this->getDoctrine()->getRepository("AppBundle:Greeting")->find($id); return new JsonResponse(['greeting' => $greeting->getGreeting()]); }
Всё, можно запускать. На экране отображается «Hello, world!». Внешне он, конечно, выглядит почти также как результат <?php echo «hello, world» ?> (если не считать бутстраповского шрифта), но теперь это современное приложение по всем канонам. Ну, скажем так, почти по всем канонам (не хватает тестов, проверок ошибок и много чего еще), но я уже задолбался это делать :)
Выводы
В последнее время сильно участились споры «зачем нужен php, если есть java». Уж не знаю, кто прав, а кто нет, холивары — дело такое. Но в каждом споре один из аргументов в пользу php — это простота для новичков. Как мне кажется, этот аргумент уже давно не валиден, что я и хотел показать этой статьёй. Новичку все равно придется кучу всего узнать и 100500 конфигов настроить: фреймворки (очень похожие на фреймворки java), базы данных, linux, javascript со всем своим зоопарком, верстка, http-протокол, различный тулинг и многое-многое другое. Даже если это не SPA.
Upd. Статья уходит в глубокий минус, но я не собираюсь менять мнение. Оно примерно такое:
1) SPA всё больше проникает в наш мир, и надо это уметь, хотя бы в общих чертах.
2) Без фреймворков не построишь хорошее современное приложение.