Содержание


Создание и отправка счетов-фактур в режиме онлайн с помощью IBM Bluemix и PHP

Создавайте и развертывайте инструмент, который заставит вас забыть о скучном и утомительном процессе выставления счетов

Comments

Как и многие профессионалы, я отправляю счета-фактуры своим клиентам как во время выполнения заданий, так и после их завершения. И, как и многие профессионалы, я считаю эту операцию скучной и утомительной, как бы она ни была необходима. В течение долгого времени моими основными инструментами для создания этих счетов-фактур были текстовый процессор и предварительно подготовленный шаблон счета, который я заполнял нужной информацией перед печатью или отправкой по электронной почте. Для ведения бухгалтерского учета и резервного копирования я также сохраняю цифровую копию каждого счета. В результате, у меня накопилась полная папка файлов счетов, и для обзора всех моих клиентов или завершенных проектов за какой-нибудь определенный год мне приходилось вручную находить и просматривать соответствующие счета.

В какой-то момент я понял, что нужно найти лучший способ. Я задумался о создании онлайн-инструмента, который будет генерировать счета-фактуры (начиная с заполнения формы), сохранять их в базе данных и отправлять их клиентам по электронной почте.

Я понимал, что для этого потребуются определенные усилия. Тем не менее, я знал, что если это сработает, создание счетов-фактур значительно упростится, а процесс обзора клиентов, проектов или доходов за любой заданный период времени станет гораздо более эффективным.

Если такой инструмент вам представляется полезным, тогда читайте дальше! Я опишу для вас пошаговый процесс создания этого инструмента и развертывания его на платформе IBM Bluemix®. Попутно я расскажу вам о двух ключевых сервисах (службах) Bluemix: сервисе базы данных ClearDB MySQL Database и сервисе хранилища объектов Object Storage.

«Если вы хотите поэкспериментировать с этим приложением, начните с интерактивного демонстрационного примера. Только имейте в виду, что это общедоступный демонстрационный пример, поэтому не загружайте в форму конфиденциальную информацию».

Запустить интерактивный демонстрационный примерПолучить код на GitHub

Что вам понадобится

Описанное здесь приложение позволяет пользователю вводить данные фактурирования в веб-форму и превращать эту информацию в счет-фактуру формата PDF. Счет-фактура сохраняется в онлайн-хранилище. Веб-ориентированная инструментальная панель позволяет пользователю просматривать список ранее сгенерированных счетов-фактур и загружать их или отправлять по электронной почте клиентам. Приложение оптимизировано для мобильных устройств, что дает пользователям возможность отправлять или просматривать счета-фактуры даже в пути (идеально подходит для пользователей, у которых нет постоянного офиса или рабочего места).

Приложение функционирует путем прозрачного для пользователя взаимодействия с различными службами и сервисами, некоторые из которых доступны непосредственно через Bluemix, а другие доступны в качестве сторонних сервисов. Вот их «короткий список»:

  • Сервис хранилища объектов Bluemix Object Storage, который предоставляет безопасное онлайн-хранилище для сгенерированных счетов-фактур
  • Сервис базы данных Bluemix ClearDB MySQL Database, который предоставляет базу данных MySQL для метаданных счетов-фактур
  • Сервис SendGrid, который предоставляет облачную платформу для быстрой и эффективной доставки электронной почты

Приложение также использует Bootstrap для создания интерфейса, оптимизированного для мобильных устройств, а также микрофреймворк Silex PHP с библиотекой mPDF для генерации счетов-фактур в формате PDF.

Кроме того, здесь используется и много других технологий. Вот перечень того, что вам понадобится:

Примечание: Любое приложение, использующее сервис SendGrid, должно соответствовать Условиям использования SendGrid Terms of Service. Аналогичным образом, любое приложение, использующее ClearDB и Object Storage, должно соответствовать условиям использования каждого из этих сервисов, как это отражено в Условиях ClearDB Terms of Service и на странице каталога Object Storage соответственно. Прежде, чем начать свой проект, потратьте немного времени на ознакомление с этими требованиями и обеспечьте соответствие им со стороны вашего приложения.

Шаг 1. Создание базового приложения

  1. Первым шагом является инициализация базового приложения с помощью микрофреймворка Silex PHP и механизма разработки шаблонов Twig. Дополнительные пакеты необходимы для генерации счетов-фактур, доставки электронной почты и доступа к хранилищу объектов. Все эти зависимости можно с легкостью загрузить и установить с помощью Composer. Используйте следующий конфигурационный файл Composer, который должен быть сохранен в $APP_ROOT/composer.json (где $APP_ROOT – это каталог вашего проекта).

    {
        "require": {
            "silex/silex": "*",
            "twig/twig": "*",
            "symfony/validator": "*",        
            "mpdf/mpdf": "*",
            "php-opencloud/openstack": "*",
            "sendgrid/sendgrid": "*"
        },
        "minimum-stability": "dev",
        "prefer-stable": true
    }
  2. Выполните установку, используя Composer, с помощью этой команды:

    shell> php composer.phar install
  3. Затем настройте основной скрипт управления для приложения. Этот скрипт загружает фреймворк Silex и инициализирует приложение Silex. Скрипт также содержит обратные вызовы для каждого маршрута приложения, причем каждый обратный вызов определяет код, который должен выполняться при сопоставлении маршрута с входящим запросом. Создайте этот скрипт в $APP_ROOT/public/index.php со следующим кодом:

    <?php
    // использовать автозагрузчик Composer
    require '../vendor/autoload.php';
    
    // загрузить конфигурацию
    require '../config.php';
    
    // загрузить классы
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Validator\Constraints as Assert;
    use Silex\Application;
    
    // инициализировать приложение Silex
    $app = new Application();
    
    // запустить отладку приложения
    // установить в false для промышленных сред
    $app['debug'] = true;
    
    // загрузить конфигурацию из файла
    $app->config = $config;
    
    // зарегистрировать поставщика шаблонов Twig
    $app->register(new Silex\Provider\TwigServiceProvider(), array(
      'twig.path' => __DIR__.'/../views',
    ));
    
    // зарегистрировать поставщика службы валидатора
    $app->register(new Silex\Provider\ValidatorServiceProvider());
    
    // зарегистрировать поставщика сеансовой службы
    $app->register(new Silex\Provider\SessionServiceProvider());
    
    // обработчики страницы индексов
    $app->get('/', function () use ($app) {
      return $app->redirect($app["url_generator"]->generate('index'));
    });
    
    // обработчик отображения списка счетов-фактур
    $app->get('/index', function () use ($app, $db) {
      return $app['twig']->render('index.twig', array('data' => $data));
    })->bind('index');
    
    // обработчик отображения формы счета-фактуры
    $app->get('/create', function () use ($app) {
      // TODO
    })->bind('create');
    
    // генератор счетов-фактур
    $app->post('/create', function (Request $request) 
      use ($app, $db, $mpdf, $objectstore) {
      // TODO
    });
    
    // обработчик запроса на удаление счета-фактуры
    $app->get('/delete/{id}', function ($id) use ($app, $db, $objectstore) {
      // TODO
    })->bind('delete');
    
    // обработчик запроса на загрузку счета-фактуры
    $app->get('/download/{id}', function ($id) use ($app, $objectstore) {
      // TODO
    })->bind('download');
    
    // обработчик запроса на доставку счета-фактуры
    $app->get('/send/{id}', function ($id) use ($app, $objectstore, $db, $sg) {
      // TODO
    })->bind('send');
    
    // запуск приложения
    $app->run();
  4. Поскольку приложение поддерживает составление списка, добавление, удаление, загрузку и отправку по электронной почте счетов-фактур, скрипт определяет URL-маршруты и «заполнители» для маршрутов /index, /create, /delete, /download, and /send. Они будут заполняться по мере прохождения нами этапов проекта. Скрипт также считывает данные конфигурации из конфигурационного файла приложения, инициализирует инструмент визуализации шаблонов Twig и регистрирует его с помощью Silex. На заключительном этапе подготовки создается простой пользовательский интерфейс на основе Bootstrap с областями заголовка, нижнего колонтитула и содержимого. Ниже приводится пример кода, который будет использоваться для всех экранных представлений приложения, описанных в последующих листингах:

    <!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">
        <title>Invoice Generator</title>
        <link rel="stylesheet" 
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
        <link rel="stylesheet" 
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
        <script 
          src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
        </script>
        <!-- HTML5 Shim и Respond.js для поддержки в IE8 элементов HTML5 и медиа-запросов -->
        <!-- ПРЕДУПРЕЖДЕНИЕ: Respond.js не работает, если вы просматриваете страницу через файл file:// -->
        <!--[если версия более ранняя, чем IE 9]>
          <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js">
          </script>
          <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js">
          </script>
        <![endif]-->    
      </head>
      <body>
    
        <div class="container">
          <div class="panel panel-default">
            <div class="panel-heading clearfix">
              <h4 class="pull-left">Invoice Generator</h4>
              <a href="{{ app.url_generator.generate('index') }}" 
                class="pull-right btn btn-primary btn">Home</a>
            </div>
          </div> 
    
          {% for message in app.session.flashbag.get('success') %}
          <div class="alert alert-success">
            <strong>Success!</strong> {{ message }}
          </div>
          {% endfor %} 
          
          {% for message in app.session.flashbag.get('error') %}
          <div class="alert alert-danger">
            <strong>Error!</strong> {{ message }}
          </div>
          {% endfor %} 
    
          <!-- --> 
          <!-- здесь содержание отдельной страницы -->
          <!-- --> 
    
        
      </body>
    </html>

Шаг 2. Создание формы для генерации счета-фактуры

  1. После вводной информации мы можем перейти к собственно разработке приложения. Первый шаг – определить форму, в которой будут храниться данные фактурирования и информация о клиенте. Определите эту форму как $APP_DIR/views/create.twig, используя следующий код:

    <form method="post" action="{{ app.url_generator.generate('create') }}">
      <div class="panel panel-default">
        <div class="panel-heading clearfix">
          <h4 class="pull-left">Customer Information</h4>
        </div>
        <div class="panel-body">
          <div class="form-group">
            <label for="color">Name</label>
            <input type="text" class="form-control" id="name" 
              name="name" required="true"></input>
          </div>
          <div class="form-group">
            <label for="address1">Address (line 1)</label>
            <input type="text" class="form-control" id="address1" 
              name="address1" required="true"></input>
          </div>
          <div class="form-group">
            <label for="address2">Address (line 2)</label>
            <input type="text" class="form-control" id="address2" 
              name="address2"></input>
          </div>
          <div class="form-group">
            <label for="city">City</label>
            <input type="text" class="form-control" id="city" 
              name="city" required="true"></input>
          </div>
          <div class="form-group">
            <label for="state">State</label>
            <input type="text" class="form-control" id="state" 
              name="state" required="true"></input>
          </div>
          <div class="form-group">
            <label for="postcode">Postal code</label>
            <input type="text" class="form-control" id="postcode" 
              name="postcode" required="true"></input>
          </div>
          <div class="form-group">
            <label for="email">Email address</label>
            <input type="email" class="form-control" id="email" 
              name="email" required="true"></input>
          </div>
        </div>
      </div>
      
      <div class="panel panel-default">
        <div class="panel-heading clearfix">
          <h4 class="pull-left">Invoice Information</h4>
        </div>
        <div class="panel-body" id="lines">
          <div id="line-template" class="line">
            <div class="form-group">
              <label>Item description</label>
              <input type="text" class="form-control" 
                name="lines[0][item]"></input>
            </div>
            <div class="form-group">
              <label>Quantity</label>
              <input type="number" min="0" step="any" class="form-control" 
                name="lines[0][qty]"></input>
            </div>
            <div class="form-group">
              <label>Rate</label>
              <input type="number" min="0" step="any" class="form-control" 
                name="lines[0][rate]"></input>
            </div>
          </div>
        </div>
      </div>
      
      <div class="form-group">
        <a id="add" href="#" class="btn btn-primary">
          <span class="glyphicon glyphicon-plus"></span>
          Add Invoice Line</a>
        <button type="submit" name="submit" class="btn btn-primary">
          Submit</button>
      </div>             
    
    </form>
  2. Эта форма в целом состоит из двух разделов: один для имени/наименования клиента, адреса и адреса электронной почты, а другой для данных счета-фактуры. Последний раздел состоит из трех полей, каждое из которых представляет одну строку счета-фактуры: поле описания позиции, поле тарифной ставки (цены по прейскуранту) и поле количества (объема). Поскольку в счетах-фактурах достаточно часто присутствует несколько строк, этот раздел можно клонировать для добавления новых строк с помощью кнопки Add Invoice Line (Добавить строку счета-фактуры). При нажатии на эту кнопку выполняется представленный ниже код JavaScript для динамического добавления в форму нового элемента строки счета-фактуры.

    <script>
    $(document).ready(function() {  
      var cloneIndex = 0;
      
      $("#add").click(function(e) {  
          e.preventDefault();
          $("#line-template").clone()
              .appendTo("#lines")
              .prepend('<hr />')
              .attr('id', null)
              .find("input").each(function() {
                  var name = this.name;
                  this.name = name.replace("lines[0]", "lines[" + (cloneIndex+1) + "]");
                  this.value = null;
          });
          cloneIndex++;    
      });
    });
    </script>
  3. Также обратите внимание, что строки счета-фактуры структурированы как многоуровневый массив: lines[0] представляет первую строку, lines[1] – вторую строку и т.д. В первой строке, lines[0][item] содержит описание позиции; lines[0][rate] – соответствующую тарифную ставку или цену по прейскуранту; и lines[0][qty] – количество или объем (как измерение количества).

    Ниже на скриншоте показано, как выглядит форма:

    Рисунок 1. Форма для генерации счета-фактуры
    Invoice generation form
    Invoice generation form

    При нажатии на кнопку Submit (Отправить) данные, введенные в форму, отправляются в виде запроса POST в обработчик обратного вызова /create. Этот обработчик обратного вызова выполняет большую часть ответственной работы в приложении, а именно:

    • Проверяет корректность ввода данных в форме
    • Рассчитывает промежуточные суммы для каждой строки счета-фактуры
    • Рассчитывает итоговую сумму для счета-фактуры
    • Создает счет-фактуру в формате PDF из заданного шаблона
    • Сохраняет счет-фактуру в выделенной области памяти хранилища
    • Добавляет запись в базу данных с основными данными счета-фактуры

Из вышесказанного следует, что перед запуском обработчика обратного вызова должны быть инициализированы база данных и область памяти, а также должен быть определен шаблон, который будет использоваться для генерации счета-фактуры. Эти операции мы рассмотрим ниже.

Шаг 3. Инициализация сервиса базы данных и сервиса хранилища на платформе Bluemix

Bluemix предлагает несколько вариантов предоставления «базы данных как сервиса». Одним из вариантов является сервис ClearDB MySQL Database. Как вы можете догадаться, этот сервис предоставляет пустой экземпляр базы данных MySQL, который может быть «привязан» к вашему приложению. План (использования сервиса) по умолчанию предлагает только ограниченную область памяти для бесплатного хранилища.

  1. Чтобы увидеть, как это работает, инициализируйте новый экземпляр сервиса ClearDB MySQL Database на Bluemix, сначала войдя в свою учетную запись Bluemix. Затем на инструментальной панели нажмите кнопку Console (Консоль). В появившемся списке сервисов выберите Data and Analytics (Данные и аналитика). Нажмите на кнопку Add (Добавить) и в списке отображаемых сервисов выберите ClearDB MySQL Database и бесплатный план. Убедитесь, что в поле Connect to (Подключить к) установлено значение Leave unbound (Оставить несвязанным), чтобы вы могли продолжить разработку приложения на отдельном хосте при наличии одного экземпляра сервиса базы данных, размещенного на Bluemix.

    Рисунок 2. Создание сервиса
    Service creation
    Service creation
  2. Теперь экземпляр сервиса базы данных ClearDB инициализирован. На странице описания сервиса откройте инструментальную панель ClearDB, выберите вкладку Endpoint Information (Информация о конечной точке) для просмотра учетных данных экземпляра сервиса и введите значения в полях Cluster name (Имя кластера), Hostname (Имя хоста), Username (Имя пользователя) и Password (Пароль) для внесения в конфигурационный файл приложения $APP_DIR/config.php.

    Рисунок 3. Учетные данные сервиса
    Service credentials
    Service credentials
  3. 3. Используйте эти учетные данные для подключения к базе данных MySQL (с помощью такого инструмента как MySQL CLI или phpMyAdmin) и создайте новую таблицу для хранения данных счета-фактуры с помощью следующего SQL-кода:

    CREATE TABLE invoices ( 
      id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, 
      ts TIMESTAMP NOT NULL, 
      name TEXT NOT NULL, 
      email VARCHAR(255) NOT NULL, 
      amount FLOAT NOT NULL 
    );
  4. Аналогичным образом инициализируйте новый экземпляр сервиса хранилища объектов Object Storage в разделе Storage (Хранилище) инструментальной панели Bluemix. Как и ранее, убедитесь, что в поле Connect to установлено значение Leave unbound.

    Рисунок 4. Создание сервиса
    Service creation
    Service creation
  5. На странице информации о сервисе выберите вкладку Service Credentials (Учетные данные сервиса) для просмотра учетных данных для экземпляра сервиса и добавьте отображаемые там значения в конфигурационный файл приложения $APP_DIR/config.php.

    Рисунок 5. Учетные данные сервиса
    Service credentials
    Service credentials
  6. Кроме того, на странице Manage (Управление) создайте новый контейнер хранилища с именем "invoices" («счета-фактуры») для хранения сгенерированных счетов-фактур.

    Рисунок 6. Создание контейнера хранилища
    Storage container creation
    Storage container creation

Шаг 4. Генерация счета-фактуры в формате PDF

  1. Следующим шагом после инициализации базы данных и системы хранения объектов будет создание шаблона для генерируемых счетов-фактур. Поскольку библиотека mPDF включает в себя мощные функции для преобразования HTML в PDF, самым простым способом является создание шаблона с использованием HTML, заполнение его фактическими данными и, затем, преобразование его в PDF.

    Ниже приводится пример кода шаблона счета-фактуры, который должен быть сохранен в $APP_DIR/views/invoice.twig:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <link rel="stylesheet" 
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
        <link rel="stylesheet" 
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
        <style>
        .panel-heading {
          background-color: #d3d3d3;
        }
        .col-md-4 {
          width: 25%;
        }
        </style>    
      </head>
      <body>
    
        <div class="page-header">
          <h1 class="text-center">SAMPLE INVOICE</h1>
        </div>
    
        <div class="container">
          <div class="col-md-4">
            <div class="panel panel-default">
              <div class="panel-heading">
                <h3 class="panel-title">CUSTOMER</h3>
              </div>
              <div class="panel-body">
                {{ data.name }} <br />
                {{ data.address1 }} <br />
                {% if data.address2 %}
                  {{ data.address2 }} <br />
                {% endif %}
                {{ data.city }} <br />
                {{ data.state }} <br />
                {{ data.postcode }} <br />
              </div>
            </div>  
          </div>
        </div>
        
        <div class="container">
          <div class="col-md-12">
            <div class="panel panel-default">
              <div class="panel-heading">
                <h3 class="panel-title">SUMMARY</h3>
              </div>
              <div class="panel-body">
                <table class="table">
                    <tr>
                      <th>Description</th>
                      <th>Quantity</th>
                      <th>Rate</th>
                      <th>Amount</th>                  
                    </tr>
                    {% for line in data.lines %}
                    <tr>
                      <td> {{ line['item'] }}</td>
                      <td> {{ line['qty'] }}</td>
                      <td> {{ line['rate'] }}</td>
                      <td> {{ line['subtotal'] }}</td>
                    </tr>
                    {% endfor %}
                    <tr>
                      <td></td>
                      <td></td>
                      <td></td>
                      <td style="border-top: solid 1px black">
                        <strong>{{ total }}</strong>
                      </td>
                    </tr>
                </table>
              </div>
            </div>  
          </div>
        </div>    
        
        <div class="container">
          <div class="col-md-6">
            <div class="panel panel-default">
              <div class="panel-heading">
                <h3 class="panel-title">PAYMENT TERMS</h3>
              </div>
              <div class="panel-body">
                <ul>
                  <li>Payment within 15 days of invoice date</li>
                  <li>Late payment penalty: 1% per month</li>
                  <li>Sample invoice, not for production use or billing</li>
                </ul>
              </div>
            </div>  
          </div>
        </div>
      
      </body>
    </html>

    Здесь все традиционно: шаблон использует Bootstrap для создания раздела информации о клиенте, за которым следует таблица с несколькими строками для данных счета-фактуры, а затем раздел условий оплаты. Очевидно, что шаблон можно с легкостью настроить для добавления (к примеру) логотипа компании в заголовок или номера заказа на покупку.

  2. Заключительный шаг – обновление обработчика обратного вызова /create для проверки корректности представленных данных, расчета промежуточных сумм строк позиции и итоговой суммы для счета-фактуры в целом, и генерации собственно счета-фактуры с использованием подготовленного шаблона. См. код ниже:

    <?php
    // загрузить классы и конфигурацию
    require '../vendor/autoload.php';
    require '../config.php';
    
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Validator\Constraints as Assert;
    use Silex\Application;
    
    // инициализировать приложение Silex
    $app = new Application();
    
    // зарегистрировать поставщиков сервисов здесь
    
    // инициализировать механизм PDF
    $mpdf = new mPDF();
    
    // инициализировать подключение к базе данных
    $db = new mysqli(
      $app->config['settings']['db']['hostname'], 
      $app->config['settings']['db']['username'], 
      $app->config['settings']['db']['password'], 
      $app->config['settings']['db']['name']
    );
    
    if ($db->connect_errno) {
      throw new Exception('Failed to connect to MySQL: ' . $db->connect_error);
    }
    
    // инициализировать клиента OpenStack
    $openstack = new OpenStack\OpenStack(array(
      'authUrl' => $app->config['settings']['object-storage']['url'],
      'region'  => $app->config['settings']['object-storage']['region'],
      'user'    => array(
        'id'       => $app->config['settings']['object-storage']['user'],
        'password' => $app->config['settings']['object-storage']['pass']
    )));
    $objectstore = $openstack->objectStoreV1();
    
    // обработчик отображения формы счета-фактуры
    $app->get('/create', function () use ($app) {
      return $app['twig']->render('create.twig');
    })->bind('create');
    
    // генератор счетов-фактур
    $app->post('/create', function (Request $request) 
      use ($app, $db, $mpdf, $objectstore) {
      // collect input parameters
      $params = array(
        'name' => strip_tags(trim(strtolower($request->get('name')))),
        'address1' => strip_tags(trim($request->get('address1'))),
        'address2' => strip_tags(trim($request->get('address2'))),
        'city' => strip_tags(trim($request->get('city'))),
        'state' => strip_tags(trim($request->get('state'))),
        'postcode' => strip_tags(trim($request->get('postcode'))),
        'email' => strip_tags(trim($request->get('email'))),
        'lines' => $request->get('lines'),
      );
      
     // определить ограничения проверки
      $constraints = new Assert\Collection(array(
        'name' => new Assert\NotBlank(array('groups' => 'invoice')),
        'address1' => new Assert\NotBlank(array('groups' => 'invoice')),
        'address2' => new Assert\Type(
          array('type' => 'string', 'groups' => 'invoice')
        ),
        'city' => new Assert\NotBlank(array('groups' => 'invoice')),
        'state' => new Assert\NotBlank(array('groups' => 'invoice')),
        'postcode' => new Assert\NotBlank(array('groups' => 'invoice')),
        'email' =>  new Assert\Email(array('groups' => 'invoice')),
        'lines' =>  new App\Validator\Constraints\Lines(
          array('groups' => 'invoice')
        ),
      ));
      
      // проверять ввод и установить ошибки, если таковые имеются, в виде флэш-сообщений
      // если есть ошибки, перенаправить на входную форму
      $errors = $app['validator']->validate(
        $params, $constraints, array('invoice')
      );
      if (count($errors) > 0) {
        foreach ($errors as $error) {
          $app['session']->getFlashBag()->add('error', 
            'Invalid input in field ' . $error->getPropertyPath() . ': ' . 
            $error->getMessage()
          );
        }
        return $app->redirect($app["url_generator"]->generate('create'));
      }  
      
      // если входные данные прошли проверку
      // рассчитать промежуточные суммы и итоговую сумму
      $total = 0;
      foreach ($params['lines'] as $lineNum => &$lineData) {
        $lineData['subtotal'] = $lineData['qty'] * $lineData['rate'];
        $total += $lineData['subtotal'];
      }
      
      // сохранить запись в MySQL
      // получить идентификатор записи
      if (!$db->query("INSERT INTO invoices (name, email, amount, ts) 
        VALUES ('" . $params['name'] . "', '" . $params['email'] . "', '" . $total . "', NOW())")) {
        $app['session']->getFlashBag()->add(
          'Failed to save invoice to database: ' . $db->error
        );
        return $app->redirect($app["url_generator"]->generate('index'));
      }
      $id = $db->insert_id;
      
      // генерировать счет-фактуру в формате PDF из шаблона
      $html = $app['twig']->render('invoice.twig', 
        array('data' => $params, 'total' => $total)
      );
      $mpdf->WriteHTML($html);
      $pdf = $mpdf->Output('', 'S'); 
    
      // сохранять PDF в контейнер с идентификатором в качестве имени
      $container = $objectstore->getContainer('invoices');
      $options = array(
        'name'   => "$id.pdf",
        'content' => $pdf,
      );
      $container->createObject($options);
      
      // отобразить сообщение об успешном завершении
      $app['session']->getFlashBag()->add('success', 
        "Invoice #$id created.");
      return $app->redirect($app["url_generator"]->generate('index'));
    });
    
    // зарегистрировать другие обработчики здесь
    
    // выполнить приложение
    $app->run();

    Здесь есть несколько интересных моментов, поэтому давайте рассмотрим этот код повнимательнее, раздел за разделом:

    1. Перед инициализацией обработчиков обратного вызова, код инициализирует объект mPDF для генерации PDF, клиентский объект MySQL для операций базы данных (через расширение PHP mysqli) и клиентский объект OpenStack для взаимодействия с API-интерфейсом объектного хранилища. Эти три объекта затем передаются в обработчик обратного вызова /create.
    2. Обработчик начинает со сбора различных входных параметров – информации о клиенте, описаний позиций, значений тарифных ставок (цен), количества – и проверки этих параметров с помощью Symfony. В частности, строки счета-фактуры проверяются с использованием валидатора Lines (доступен в репозитории исходного кода приложения), который проверяет каждую строку счета-фактуры на корректность и полноту ввода, а также на то, что тарифные ставки (цены) и количество (объем) указаны как числовые значения.
    3. Если ввод выполнен корректно, для каждой позиции рассчитывается промежуточная сумма. Разные промежуточные суммы, в свою очередь, суммируются для получения итоговой суммы для счета-фактуры.
    4. Клиентский объект MySQL используется для генерации и вставки в базу данных приложения новой записи, представляющей новый счет-фактуру. Запись базы данных содержит имя и адрес электронной почты клиента, общую сумму счета и дату создания. Каждая такая запись будет иметь уникальный, автоматически сгенерированный идентификатор.
    5. Различные входные переменные включаются в шаблон счета-фактуры, чтобы создать счет-фактуру в формате HTML с помощью метода render() механизма шаблонов Twig. Счет-фактура в формате HTML затем конвертируется в формат PDF с использованием методов WriteHTML() и Output(), которые в конечном итоге возвращает строку байтов, представляющую PDF-файл.
    6. Метод getContainer() клиентского объекта OpenStack используется для получения ссылки на созданный ранее контейнер "invoices" («счета-фактуры»), а метод createObject() используется для сохранения счета-фактуры формата PDF в контейнере. Обратите внимание, что имя файла счета-фактуры в формате PDF устанавливается в соответствии с автоматически сгенерированным идентификатором соответствующей записи в базе данных MySQL.

Предполагая, что все описанные выше шаги успешно выполнены, клиентский браузер перенаправляется обратно на страницу индекса приложения с уведомлением об успешном завершении.

Вот как выглядит пример счета-фактуры в формате PDF:

Рисунок 7. Пример счета-фактуры в формате PDF
Sample PDF invoice
Sample PDF invoice

Шаг 5. Реализация функций загрузки и отправки счета-фактуры

Для окончательной реализации процесса генерации счета-фактуры осталось добавить в приложение ряд функций инструментальной панели. В частности, добавим обработчики обратного вызова, чтобы пользователи могли просматривать, загружать, удалять счета-фактуры и отправлять их по электронной почте.

  1. Обработчики /index и /delete используют клиентские объекты MySQL и OpenStack для отображения и удаления счетов-фактур. Обратите внимание, что при удалении счетов-фактур они должны быть удалены как из базы данных, так и из объектного хранилища.

    <?php
    // ...
    
    // обработчик отображения списка счетов-фактур
    $app->get('/index', function () use ($app, $db) {
      $result = $db->query("SELECT * FROM invoices ORDER BY ts DESC");
      $data = $result->fetch_all(MYSQLI_ASSOC);
      return $app['twig']->render('index.twig', array('data' => $data));
    })->bind('index');
    
    // обработчик запроса на удаление счета-фактуры
    $app->get('/delete/{id}', function ($id) use ($app, $db, $objectstore) {
      // delete invoice from database
      if (!$db->query("DELETE FROM invoices WHERE id = '$id'")) {
        $app['session']->getFlashBag()->add(
          'Failed to delete invoice from database: ' . $db->error
        );
        return $app->redirect($app["url_generator"]->generate('index'));
      }
      // удаление счета-фактуры из хранилища объектов
      $container = $objectstore->getContainer('invoices');
      $object = $container->getObject("$id.pdf");
      $object->delete();  
      $app['session']->getFlashBag()->add('success', "Invoice #$id deleted.");
      return $app->redirect($app["url_generator"]->generate('index'));  
    })->bind('delete');
  2. Инструментальная панель также должна дать пользователям возможность загружать ранее созданные счета-фактуры на их рабочий стол посредствам маршрута /download – задачи, легко выполняемой с помощью методов getObject() и download() клиентского объекта OpenStack. Полученный файл затем отправляется в браузер клиента (которому выставляется счет-фактура) в виде вложения с соответствующими заголовками ответа.

    <?php
    // ...
    
    // обработчик запроса загрузки счета-фактуры
    $app->get('/download/{id}', function ($id) use ($app, $objectstore) {
      // получить файл счета-фактуры
      $file = $objectstore->getContainer('invoices')
                          ->getObject("$id.pdf")
                          ->download();
      // установить заголовки и тело ответа
      // отправить файл клиенту
      $response = new Response();
      $response->headers->set('Content-Type', 'application/pdf');
      $response->headers->set('Content-Disposition', 'attachment; 
        filename="' . $id .'.pdf"'
      );
      $response->headers->set('Content-Length', $file->getSize());
      $response->headers->set('Expires', '@0');
      $response->headers->set('Cache-Control', 'must-revalidate');
      $response->headers->set('Pragma', 'public');
      $response->setContent($file);
      return $response;
    })->bind('download');
  3. Для отправки счета-фактуры по электронной почте требуется небольшая дополнительная помощь от почтового сервиса SendGrid. Если у вас уже есть учетная запись SendGrid, войдите в систему и создайте ключ API через меню Settings (Настройки) -> API Keys (Ключи API).

    Рисунок 8. Генерация API-ключа SendGrid
    SendGrid API key generation
    SendGrid API key generation

    Добавьте этот ключ API в конфигурационный файл приложения в $APP_DIR/config.php, а затем добавьте следующий код в файл $APP_DIR/public/index.php:

    <?php
    // ...
    
    // зарегистрировать поставщиков сервисов
    // инициализировать клиента SendGrid
    $sg = new \SendGrid($app->config['settings']['sendgrid']['key']);
    
    //...
    
    // обработчик запроса доставки счета-фактуры
    $app->get('/send/{id}', function ($id) use ($app, $objectstore, $db, $sg) {
      // получить файл счета-фактуры
      $file = $objectstore->getContainer('invoices')
                          ->getObject("$id.pdf")
                          ->download();
      // изменить это на любой реальный адрес электронной почты                    
      $from = new SendGrid\Email(null, "no-reply@example.com");  
      $subject = "Invoice #$id";
      $result = $db->query("SELECT email FROM invoices WHERE id = '$id'");
      $row = $result->fetch_assoc();
      $to = new SendGrid\Email(null, $row['email']);
      $content = new SendGrid\Content(
        "text/plain", 
        "Please note that the attached sample invoice is generated 
        for demonstration purposes only and no payment is required."
      );
      $mail = new SendGrid\Mail($from, $subject, $to, $content);
      $attachment = new SendGrid\Attachment();
      $attachment->setContent(base64_encode($file));
      $attachment->setType("application/pdf");
      $attachment->setFilename("invoice_$id.pdf");
      $attachment->setDisposition("attachment");
      $attachment->setContentId("invoice_$id");
      $mail->addAttachment($attachment);
      $response = $sg->client->mail()->send()->post($mail);
      if ($response->statusCode() == 200 || $response->statusCode() == 202) {
        $app['session']->getFlashBag()->add('success', "Invoice #$id sent.");  
      } else {
        $app['session']->getFlashBag()->add('error', "Failed to send invoice.");    
      }
      return $app->redirect($app["url_generator"]->generate('index'));  
    })->bind('send');

    Обработчик /email использует клиентский объект PHP SendGrid (инициализированный в верхней части скрипта). Он подключается к базе данных для извлечения адреса электронной почты клиента для указанного номера счета-фактуры и, затем, извлекает фактический счет-фактуру из хранилища объектов с помощью метода download() в OpenStack. После этого он использует клиентский объект SendGrid для создания новых объектов электронной почты (Email) и вложений (Attachment), и доставляет их на указанный адрес электронной почты клиента с помощью почтового сервиса SendGrid.

Шаг 6. Развертывание на IBM Bluemix

  1. На этом этапе создание приложения завершено, и оно может быть развернуто на Bluemix. Для этого сначала создайте файл манифеста приложения по адресу $APP_ROOT/manifest.yml, не забывая использовать уникальное имя хоста и приложения, добавив к нему случайную строку (например, ваши инициалы).

    ---
    applications:
    - name: invoice-generator-[initials]
    memory: 256M
    instances: 1
    host: invoice-generator-[initials]
    buildpack: https://github.com/cloudfoundry/php-buildpack.git
    stack: cflinuxfs2
  2. Пакет сборки Cloud Foundry PHP по умолчанию не включает в своем составе расширение PHP MySQLi или расширение curl (используемое клиентом OpenStack), поэтому при настройке во время развертывания включите эти расширения в пакет сборки. Аналогичным образом необходимо настроить пакет сборки для использования каталога public приложения в качестве каталога веб-сервера. Создайте файл $APP_ROOT/.bp-config/options.json со следующим содержимым:

    {
        "WEB_SERVER": "httpd",
        "PHP_EXTENSIONS": ["bz2", "zlib", "mysqli", "curl"],
        "COMPOSER_VENDOR_DIR": "vendor",
        "WEBDIR": "public",
        "PHP_VERSION": "{PHP_56_LATEST}"
    }
  3. Кроме того, чтобы получить учетные данные для сервисов ClearDB и Object Storage, автоматически извлекаемые с Bluemix, обновите скрипт $APP_ROOT/public/index.php для использования переменной VCAP_SERVICES Bluemix, как показано ниже:

    <?php                
    // включить автозагрузчик и конфигурацию
    require '../vendor/autoload.php';
    require '../config.php';
                    
    // если доступна среда BlueMix VCAP_SERVICES
    // перезаписать локальные учетные данные с помощью учетных данных BlueMix
    if ($services = getenv("VCAP_SERVICES")) {
      $services_json = json_decode($services, true);
      $app->config['settings']['db']['hostname'] = 
        $services_json['cleardb'][0]['credentials']['hostname'];
      $app->config['settings']['db']['username'] = 
        $services_json['cleardb'][0]['credentials']['username'];
      $app->config['settings']['db']['password'] = 
        $services_json['cleardb'][0]['credentials']['password'];
      $app->config['settings']['db']['name'] = 
        $services_json['cleardb'][0]['credentials']['name'];
      $app->config['settings']['object-storage']['url'] = 
        $services_json["Object-Storage"][0]["credentials"]["auth_url"] . '/v3';
      $app->config['settings']['object-storage']['region'] = 
        $services_json["Object-Storage"][0]["credentials"]["region"];
      $app->config['settings']['object-storage']['user'] = 
        $services_json["Object-Storage"][0]["credentials"]["userId"];
      $app->config['settings']['object-storage']['pass'] = 
        $services_json["Object-Storage"][0]["credentials"]["password"];  
    } 
    
    // ...
    
    // инициализировать приложение Silex
    $app = new Application();
    
    // ...
  4. Теперь вы можете отправить приложение на Bluemix, а затем связать с ним сервисы ClearDB и Object Storage, которые вы ранее инициализировали. Не забудьте использовать корректный идентификатор для каждого экземпляра сервиса, чтобы обеспечить их корректную привязку к приложению.

    shell> cf api https://api.ng.bluemix.net
    shell> cf login
    shell> cf push
    shell> cf bind-service invoice-generator-[initials] "ClearDB MySQL Database-[id]"
    shell> cf bind-service invoice-generator-[initials] "Object Storage-[id]"
    shell> cf restage invoice-generator-[initials]
  5. Вы можете начать использовать приложение, перейдя на хост, указанный в манифесте приложения, например, http://invoice-generator--[инициалы].mybluemix.net. Если вы видите пустую страницу или другие ошибки, прочитайте небольшую статью "Debugging PHP errors on IBM Bluemix" («Отладка ошибок PHP на платформе IBM Bluemix»), чтобы узнать, как выполнить отладку вашего кода PHP и выявить ошибки.

Заключение

В этой статье основное внимание было уделено использованию Bluemix и PHP для быстрого создания веб-ориентированного приложения, генерирующего счета-фактуры в онлайн-режиме. Объединив обычные фреймворки PHP и библиотеки с открытым исходным кодом с высокодоступными и масштабируемыми сервисами платформы Bluemix для управления и хранения данных, я постарался продемонстрировать, насколько простой задачей для разработчиков является создание и запуск полезных бизнес-решений в облаке.

Если вы хотите поэкспериментировать с этим приложением, начните с интерактивной онлайн-демонстрации. Просто не забывайте, что демонстрация является общедоступной, поэтому не загружайте в форму конфиденциальную информацию. Вы можете использовать кнопку Reset System (Сброс системы), чтобы удалить все загруженные данные. Затем загрузите исходный код с Github и повнимательнее посмотрите, как он работает.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Облачные вычисления
ArticleID=1050807
ArticleTitle=Создание и отправка счетов-фактур в режиме онлайн с помощью IBM Bluemix и PHP
publish-date=10112017