«Hello, (real) world!» на php в 2017 году

в 7:48, , рубрики: hello world, java, javascript, npm, php, React, ReactJS, redux, symfony, webpack, Разработка веб-сайтов

«Hello, (real) world!» на php в 2017 году - 1Вы наверняка думаете, что писать на 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 AppBundleEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 * @ORMTable(name="greetings")
 */
class Greeting
{
    /**
     * @ORMColumn(type="integer")
     * @ORMId
     */
    private $id;

    /**
     * @ORMColumn(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) Без фреймворков не построишь хорошее современное приложение.

Автор: varanio

Источник

Поделиться

* - обязательные к заполнению поля