feat: Make MJML API endpoint configurable

Replaces qferr/mjml-php with kumi/mjml-php fork.
Adds field to settings for specifying API URL.
Prevents sending API requests if neither credentials nor custom URL specified.
This commit is contained in:
Kumi 2025-03-10 17:41:04 +01:00
parent 003abbbf98
commit 9e72bba7ca
Signed by: kumi
GPG key ID: ECBCC9082395383F
37 changed files with 177 additions and 4018 deletions

View file

@ -3,6 +3,7 @@
### Added
- Support for PHP 8.3
- Support for custom MJML API URLs (Private.coffee fork only)
### Fixed

View file

@ -1,12 +1,18 @@
{
"repositories": [
{
"type": "vcs",
"url": "https://git.private.coffee/kumi/mjml-php.git"
}
],
"require": {
"embed/embed": "^4.4",
"qferr/mjml-php": "^2.0",
"gettext/gettext": "^5.6",
"gettext/translator": "^1.1",
"jaybizzle/crawler-detect": "^1.2",
"slim/psr7": "^1.6",
"erusev/parsedown": "^1.7"
"erusev/parsedown": "^1.7",
"kumi/mjml-php": "dev-main"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",

104
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d2667894e4108f2ea9ece2a5b6a85243",
"content-hash": "939c1896caf454544982bfefec1310cc",
"packages": [
{
"name": "composer/ca-bundle",
@ -551,6 +551,55 @@
},
"time": "2024-06-07T07:58:43+00:00"
},
{
"name": "kumi/mjml-php",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://git.private.coffee/kumi/mjml-php.git",
"reference": "b8ce0c35cc3376b5fc5ecd913a6ea701d2de762d"
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Qferrer\\Mjml\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Qferrer\\Tests\\Mjml\\": "tests/"
}
},
"scripts": {
"test": [
"phpunit tests/"
]
},
"license": [
"MIT"
],
"authors": [
{
"name": "Quentin",
"email": "qferrer@outook.com"
},
{
"name": "Kumi",
"email": "mjml-php@kumi.email"
}
],
"description": "A simple PHP library to render MJML to HTML.",
"time": "2025-03-10T15:03:58+00:00"
},
{
"name": "ml/iri",
"version": "1.1.4",
@ -868,51 +917,6 @@
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "qferr/mjml-php",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/qferr/mjml-php.git",
"reference": "c7185024d8b561bd3b532b29f46cba0f4b789b70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/qferr/mjml-php/zipball/c7185024d8b561bd3b532b29f46cba0f4b789b70",
"reference": "c7185024d8b561bd3b532b29f46cba0f4b789b70",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Qferrer\\Mjml\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Quentin",
"email": "qferrer@outook.com"
}
],
"description": "A simple PHP library to render MJML to HTML.",
"support": {
"issues": "https://github.com/qferr/mjml-php/issues",
"source": "https://github.com/qferr/mjml-php/tree/2.0.0"
},
"time": "2022-04-09T21:34:38+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
@ -3917,10 +3921,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"kumi/mjml-php": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View file

@ -44,9 +44,16 @@ class Email
{
global $options;
// Break if MJML API is not configured
if (!$options->getOption('mjml_api_application_id') && !$options->getOption('mjml_api_url')) {
return false;
}
$api = new \Qferrer\Mjml\Http\CurlApi(
$options->getOption('mjml_api_application_id'),
$options->getOption('mjml_api_secret_key')
$options->getOption('mjml_api_secret_key'),
null,
$options->getOption('mjml_api_url')
);
$renderer = new \Qferrer\Mjml\Renderer\ApiRenderer($api);

View file

@ -17,6 +17,11 @@ if (isset($_POST['mjml_api'], $_POST['api_application_id'], $_POST['mjml_api_sec
$options->setOption('mjml_api_application_id', $_POST['api_application_id']);
$options->setOption('mjml_api_secret_key', $_POST['mjml_api_secret_key']);
}
if (isset($_POST['mjml_api'])) {
// In contrast to the two before, this one may reasonably be set to an empty string.
$options->setOption('mjml_api_url', $_POST['api_url']);
}
?>
<main>
@ -29,6 +34,15 @@ if (isset($_POST['mjml_api'], $_POST['api_application_id'], $_POST['mjml_api_sec
<h3 class="ui header"><?= __('API') ?></h3>
<form class="ui form" method="POST">
<div class="field">
<label><?=__('API URL') ?></label>
<input type="text"
name="api_url"
placeholder="https://api.mjml.io/v1/"
value="<?= $options->getOption('mjml_api_url'); ?>"
/>
</div>
<div class="field">
<label><?= __('Application ID') ?></label>
<input type="text"

View file

@ -32,6 +32,11 @@ class InstalledVersions
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
@ -309,6 +314,12 @@ class InstalledVersions
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
@ -322,19 +333,27 @@ class InstalledVersions
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = strtr(__DIR__, '\\', '/');
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
@ -350,7 +369,7 @@ class InstalledVersions
}
}
if (self::$installed !== array()) {
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}

View file

@ -15,8 +15,8 @@ return array(
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Slim\\Psr7\\' => array($vendorDir . '/slim/psr7/src'),
'Qferrer\\Mjml\\' => array($vendorDir . '/qferr/mjml-php/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'),
'Qferrer\\Mjml\\' => array($vendorDir . '/kumi/mjml-php/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),

View file

@ -120,12 +120,12 @@ class ComposerStaticInit5f3db9fc1d0cf1dd6a77a1d84501b4b1
),
'Qferrer\\Mjml\\' =>
array (
0 => __DIR__ . '/..' . '/qferr/mjml-php/src',
0 => __DIR__ . '/..' . '/kumi/mjml-php/src',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-message/src',
1 => __DIR__ . '/..' . '/psr/http-factory/src',
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Psr\\Http\\Client\\' =>
array (

View file

@ -650,6 +650,58 @@
},
"install-path": "../jaybizzle/crawler-detect"
},
{
"name": "kumi/mjml-php",
"version": "dev-main",
"version_normalized": "dev-main",
"source": {
"type": "git",
"url": "https://git.private.coffee/kumi/mjml-php.git",
"reference": "b8ce0c35cc3376b5fc5ecd913a6ea701d2de762d"
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5"
},
"time": "2025-03-10T15:03:58+00:00",
"default-branch": true,
"type": "library",
"installation-source": "source",
"autoload": {
"psr-4": {
"Qferrer\\Mjml\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Qferrer\\Tests\\Mjml\\": "tests/"
}
},
"scripts": {
"test": [
"phpunit tests/"
]
},
"license": [
"MIT"
],
"authors": [
{
"name": "Quentin",
"email": "qferrer@outook.com"
},
{
"name": "Kumi",
"email": "mjml-php@kumi.email"
}
],
"description": "A simple PHP library to render MJML to HTML.",
"install-path": "../kumi/mjml-php"
},
{
"name": "marcocesarato/php-conventional-changelog",
"version": "1.17.2",
@ -1982,54 +2034,6 @@
},
"install-path": "../psr/http-message"
},
{
"name": "qferr/mjml-php",
"version": "2.0.0",
"version_normalized": "2.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/qferr/mjml-php.git",
"reference": "c7185024d8b561bd3b532b29f46cba0f4b789b70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/qferr/mjml-php/zipball/c7185024d8b561bd3b532b29f46cba0f4b789b70",
"reference": "c7185024d8b561bd3b532b29f46cba0f4b789b70",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5"
},
"time": "2022-04-09T21:34:38+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Qferrer\\Mjml\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Quentin",
"email": "qferrer@outook.com"
}
],
"description": "A simple PHP library to render MJML to HTML.",
"support": {
"issues": "https://github.com/qferr/mjml-php/issues",
"source": "https://github.com/qferr/mjml-php/tree/2.0.0"
},
"install-path": "../qferr/mjml-php"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",

View file

@ -100,6 +100,17 @@
'aliases' => array(),
'dev_requirement' => false,
),
'kumi/mjml-php' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b8ce0c35cc3376b5fc5ecd913a6ea701d2de762d',
'type' => 'library',
'install_path' => __DIR__ . '/../kumi/mjml-php',
'aliases' => array(
0 => '9999999-dev',
),
'dev_requirement' => false,
),
'marcocesarato/php-conventional-changelog' => array(
'pretty_version' => '1.17.2',
'version' => '1.17.2.0',
@ -298,15 +309,6 @@
0 => '1.0|2.0|3.0',
),
),
'qferr/mjml-php' => array(
'pretty_version' => '2.0.0',
'version' => '2.0.0.0',
'reference' => 'c7185024d8b561bd3b532b29f46cba0f4b789b70',
'type' => 'library',
'install_path' => __DIR__ . '/../qferr/mjml-php',
'aliases' => array(),
'dev_requirement' => false,
),
'ralouphie/getallheaders' => array(
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',

View file

@ -1,53 +0,0 @@
name: PHPUnit
on: ['push']
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1']
steps:
- uses: actions/checkout@v3
# https://github.com/shivammathur/setup-php (community)
- name: Setup PHP, extensions and composer with shivammathur/setup-php
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: json, curl
- uses: actions/setup-node@v3
with:
node-version: '14'
- name: Install JS dependencies
run: npm ci
- name: Validate composer.json and composer.lock
run: composer validate --strict
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Install dependencies
run: composer update --no-interaction --no-scripts --no-progress
- name: Run test suite
run: composer run-script test

View file

@ -1,6 +0,0 @@
.idea/
vendor/
node_modules/
phpunit.xml
.phpunit.result.cache
.php-cs-fixer.cache

View file

@ -1,78 +0,0 @@
MJML in PHP
===========
![PHPUnit](https://github.com/qferr/mjml-php/actions/workflows/php.yml/badge.svg)
A simple PHP library to render MJML to HTML.
There are two ways for integrating MJML in PHP:
* using the MJML API
* using the MJML library
### Installation
```shell script
composer require qferr/mjml-php
```
### Using MJML library
Install the MJML library:
```shell script
npm install mjml --save
```
If you want a specific version, use the following syntax: `npm install mjml@4.7.1 --save`
```php
<?php
require_once 'vendor/autoload.php';
$renderer = new \Qferrer\Mjml\Renderer\BinaryRenderer(__DIR__ . '/node_modules/.bin/mjml');
$html = $renderer->render('
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello world</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
');
```
### Using MJML API
```php
<?php
require_once 'vendor/autoload.php';
$apiId = 'abcdef-1234-5678-ghijkl';
$secretKey = 'ghijkl-5678-1234-abcdef';
$api = new \Qferrer\Mjml\Http\CurlApi($apiId, $secretKey);
$renderer = new \Qferrer\Mjml\Renderer\ApiRenderer($api);
$html = $renderer->render('
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello world</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
');
```
You can get the version of MJML used by the API to transpile:
```php
$api->getMjmlVersion();
```
More details in the API documentation: [https://mjml.io/api/documentation](https://mjml.io/api/documentation)

View file

@ -1,38 +0,0 @@
{
"name": "qferr/mjml-php",
"description": "A simple PHP library to render MJML to HTML.",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Quentin",
"email": "qferrer@outook.com"
}
],
"require": {
"php": ">=7.2",
"ext-curl": "*",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^8.5"
},
"autoload": {
"psr-4": {
"Qferrer\\Mjml\\" : "src/"
}
},
"autoload-dev": {
"psr-4": {
"Qferrer\\Tests\\Mjml\\" : "tests/"
}
},
"scripts": {
"test": "phpunit tests/"
},
"config": {
"platform": {
"php": "7.2.30"
}
}
}

1806
vendor/qferr/mjml-php/composer.lock generated vendored

File diff suppressed because it is too large Load diff

1248
vendor/qferr/mjml-php/package-lock.json generated vendored

File diff suppressed because it is too large Load diff

View file

@ -1,12 +0,0 @@
{
"name": "mjml-php",
"description": "MJML in PHP",
"author": "Quentin Ferrer",
"bugs": {
"url": "https://github.com/qferr/mjml-php/issues"
},
"homepage": "https://github.com/qferr/mjml-php#readme",
"devDependencies": {
"mjml": "^4.12.0"
}
}

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.3/phpunit.xsd"
backupGlobals="false"
colors="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -1,26 +0,0 @@
<?php
namespace Qferrer\Mjml;
use Qferrer\Mjml\Exception\ApiException;
interface ApiInterface
{
/**
* Renders a MJML to HTML content.
*
* @param string $mjml The MJML content
*
* @return string The generated HTML
*
* @throws ApiException
*/
public function getHtml(string $mjml): string;
/**
* Returns the version of MJML used by the API to transpile.
*
* @return string
*/
public function getMjmlVersion(): string;
}

View file

@ -1,7 +0,0 @@
<?php
namespace Qferrer\Mjml\Exception;
class ApiException extends \RuntimeException
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Qferrer\Mjml\Exception;
class CurlException extends \RuntimeException
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Qferrer\Mjml\Exception;
class ProcessException extends \RuntimeException
{
}

View file

@ -1,31 +0,0 @@
<?php
namespace Qferrer\Mjml\Http;
use Qferrer\Mjml\Exception\CurlException;
class Curl implements CurlInterface
{
public function request(string $url, array $options = []): CurlResponseInterface
{
$request = @curl_init($url);
if (false === $request) {
throw new CurlException('Unable to initialize Curl.');
}
curl_setopt_array($request, $options);
$data = curl_exec($request);
if (false === $data) {
throw new CurlException(sprintf('Curl Error: "%s"', curl_error($request)));
}
$response = new CurlResponse($data, curl_getinfo($request, CURLINFO_HTTP_CODE));
curl_close($request);
return $response;
}
}

View file

@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
namespace Qferrer\Mjml\Http;
use Qferrer\Mjml\ApiInterface;
use Qferrer\Mjml\Exception\ApiException;
/**
* @see https://mjml.io/api/documentation/
*/
final class CurlApi implements ApiInterface
{
protected $apiEndpoint = "https://api.mjml.io/v1";
protected $appId;
protected $secretKey;
protected $credentials;
protected $curl;
public function __construct(string $appId, string $secretKey, ?CurlInterface $curl = null)
{
if (!\extension_loaded('curl')) {
throw new \LogicException(sprintf(
'You cannot use the "%s" as the "curl" extension is not installed.',
CurlApi::class
));
}
$this->appId = $appId;
$this->secretKey = $secretKey;
$this->credentials = base64_encode("$appId:$secretKey");
$this->curl = $curl ?? new Curl();
}
public function getHtml(string $mjml): string
{
$data = $this->getResult($mjml);
return $data['html'] ?? '';
}
public function getMjmlVersion(): string
{
$data = $this->getResult('<mjml></mjml>');
return $data['mjml_version'] ?? 'unknown';
}
private function getResult(string $mjml): array
{
$response = $this->curl->request($this->apiEndpoint . '/render', [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "UTF-8",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['mjml' => $mjml]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Basic {$this->credentials}"
]
]);
$data = @json_decode($response->getContent(), true);
if (false === $data || null === $data) {
throw new ApiException(sprintf(
'Unable to decode the JSON response: "%s".',
json_last_error_msg()
));
}
$httpCode = $response->getStatusCode();
if ($httpCode !== 200) {
throw new ApiException(sprintf(
'Unexpected HTTP code: %s. Api Error Message: "%s".',
$httpCode,
$data['message'] ?? 'Unknown Error'
));
}
return $data;
}
}

View file

@ -1,8 +0,0 @@
<?php
namespace Qferrer\Mjml\Http;
interface CurlInterface
{
public function request(string $url, array $options = []): CurlResponseInterface;
}

View file

@ -1,25 +0,0 @@
<?php
namespace Qferrer\Mjml\Http;
class CurlResponse implements CurlResponseInterface
{
private $data;
private $statusCode;
public function __construct($data, int $statusCode)
{
$this->data = $data;
$this->statusCode = $statusCode;
}
public function getContent()
{
return $this->data;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace Qferrer\Mjml\Http;
interface CurlResponseInterface
{
/**
* @return mixed
*/
public function getContent();
public function getStatusCode(): int;
}

View file

@ -1,91 +0,0 @@
<?php
namespace Qferrer\Mjml\Process;
use Qferrer\Mjml\Exception\ProcessException;
class Process
{
private $command;
private $input;
private $output;
private $errorMessage = '';
public const STDIN = 0;
public const STDOUT = 1;
public const STDERR = 2;
private static $descriptors = [
self::STDIN => ['pipe', 'r'],
self::STDOUT => ['pipe', 'w'],
self::STDERR => ['pipe', 'w']
];
public function __construct(string $command, string $input)
{
$this->command = $command;
$this->input = $input;
}
public function run(): void
{
$this->initialize();
$pipes = [];
$process = $this->createProcess($pipes);
$this->setInput($pipes);
$this->setOutput($pipes);
$this->setErrorMessage($pipes);
if (0 !== proc_close($process)) {
throw new ProcessException($this->errorMessage);
}
}
public function getOutput()
{
return $this->output;
}
private function initialize(): void
{
$this->output = null;
$this->errorMessage = '';
}
/**
* @return resource
*/
private function createProcess(array &$pipes)
{
$process = proc_open($this->command, self::$descriptors, $pipes);
if (!is_resource($process)) {
throw new ProcessException('Unable to create a process.');
}
return $process;
}
private function setInput(array $pipes): void
{
$stdin = $pipes[self::STDIN];
fwrite($stdin, $this->input);
fclose($stdin);
}
private function setOutput(array $pipes): void
{
$stdout = $pipes[self::STDOUT];
$this->output = stream_get_contents($stdout);
fclose($stdout);
}
private function setErrorMessage(array $pipes): void
{
$stderr = $pipes[self::STDERR];
$this->errorMessage = stream_get_contents($stderr);
fclose($stderr);
}
}

View file

@ -1,37 +0,0 @@
<?php
namespace Qferrer\Mjml\Renderer;
use Qferrer\Mjml\ApiInterface;
use Qferrer\Mjml\RendererInterface;
/**
* Class ApiRenderer
*/
class ApiRenderer implements RendererInterface
{
/**
* The HTTP client.
*
* @var ApiInterface
*/
private $client;
/**
* ApiRenderer constructor.
*
* @param ApiInterface $httpClient
*/
public function __construct(ApiInterface $httpClient)
{
$this->client = $httpClient;
}
/**
* @inheritDoc
*/
public function render(string $content): string
{
return $this->client->getHtml($content);
}
}

View file

@ -1,46 +0,0 @@
<?php
namespace Qferrer\Mjml\Renderer;
use Qferrer\Mjml\Process\Process;
use Qferrer\Mjml\RendererInterface;
/**
* Class BinaryRenderer
*/
class BinaryRenderer implements RendererInterface
{
/**
* The MJML CLI path.
*
* @var string
*/
private $bin;
/**
* @var string
*/
private $command;
/**
* BinaryRenderer constructor.
*
* @param string $bin
*/
public function __construct(string $bin)
{
$this->bin = $bin;
$this->command = "{$this->bin} -i -s --config.validationLevel --config.minify";
}
/**
* @inheritDoc
*/
public function render(string $content): string
{
$process = new Process($this->command, $content);
$process->run();
return $process->getOutput();
}
}

View file

@ -1,18 +0,0 @@
<?php
namespace Qferrer\Mjml;
/**
* Interface RendererInterface
*/
interface RendererInterface
{
/**
* Renders MJML to HTML content.
*
* @param string $content The MJML content
*
* @return string The generated HTML
*/
public function render(string $content): string;
}

View file

@ -1,94 +0,0 @@
<?php
namespace Qferrer\Tests\Mjml\Http;
use PHPUnit\Framework\TestCase;
use Qferrer\Mjml\Http\CurlApi;
use Qferrer\Mjml\Exception\ApiException;
use Qferrer\Mjml\Http\CurlInterface;
use Qferrer\Mjml\Http\CurlResponseInterface;
class CurlApiTest extends TestCase
{
/**
* @var \PHPUnit\Framework\MockObject\MockObject|CurlInterface
*/
private $curl;
private $api;
private $response;
protected function setUp(): void
{
$this->response = $this->createMock(CurlResponseInterface::class);
$this->curl = $this->createMock(CurlInterface::class);
$this->curl->method('request')->willReturn($this->response);
$this->api = new CurlApi('test', 'mysecret', $this->curl);
}
public function testGetHtml()
{
$html = '<p>Hello World</p>';
$this->configureResponseMock(['html' => $html], 200);
$this->curl->method('request')->with('https://api.mjml.io/v1/render', [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "UTF-8",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['mjml' => 'test']),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Basic " . base64_encode("test:mysecret")
]
]);
$this->assertEquals($html, $this->api->getHtml('test'));
}
public function testGetHtmlWithInvalidStatusCode()
{
$this->configureResponseMock([], 400);
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Unexpected HTTP code: 400. Api Error Message: "Unknown Error"');
$this->api->getHtml('test');
}
public function testGetHtmlWithInvalidStatusCodeAndErrorMessage()
{
$this->configureResponseMock(['message' => 'Invalid JSON'], 400);
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Unexpected HTTP code: 400. Api Error Message: "Invalid JSON"');
$this->api->getHtml('test');
}
public function testGetHtmlWithInvalidContent()
{
$this->configureResponseMock('{invalid json}', 200);
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Unable to decode the JSON response: "Syntax error".');
$this->api->getHtml('test');
}
public function testGetMjmlVersion()
{
$this->configureResponseMock(['mjml_version' => '4.4.0'], 200);
$this->assertEquals('4.4.0', $this->api->getMjmlVersion());
}
private function configureResponseMock($data, int $statusCode)
{
if (is_array($data)) {
$data = json_encode($data);
}
$this->response->method('getContent')->willReturn($data);
$this->response->method('getStatusCode')->willReturn($statusCode);
}
}

View file

@ -1,30 +0,0 @@
<?php
namespace Qferrer\Tests\Mjml\Renderer;
use PHPUnit\Framework\TestCase;
use Qferrer\Mjml\ApiInterface;
use Qferrer\Mjml\Http\CurlApi;
use Qferrer\Mjml\Renderer\ApiRenderer;
class ApiRendererTests extends TestCase
{
private $httpClient;
private $apiRenderer;
protected function setUp(): void
{
$this->httpClient = $this->createMock(ApiInterface::class);
$this->apiRenderer = new ApiRenderer($this->httpClient);
}
public function testRender()
{
$expectedHtml = '<p>Test</p>';
$this->httpClient->expects($this->once())->method('getHtml')->willReturn($expectedHtml);
$html = $this->apiRenderer->render('<mjml>Test</mjml>');
$this->assertEquals($expectedHtml, $html);
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace Qferrer\Tests\Mjml\Renderer;
use PHPUnit\Framework\TestCase;
use Qferrer\Mjml\Exception\ProcessException;
use Qferrer\Mjml\Renderer\BinaryRenderer;
use Qferrer\Tests\Mjml\ResourcesTrait;
class BinaryRendererTest extends TestCase
{
use ResourcesTrait;
public function testRender()
{
$renderer = new BinaryRenderer(__DIR__ . '/../../node_modules/.bin/mjml');
$html = $renderer->render($this->loadResource('hello_world.mjml'));
$this->assertEquals($this->loadResource('hello_world.min.html.text'), $html);
}
public function testRenderWithInvalidBinaryThrowException()
{
$this->expectException(ProcessException::class);
$this->expectExceptionMessage('unknown');
$renderer = new BinaryRenderer('unknown');
$renderer->render($this->loadResource('hello_world.mjml'));
}
public function testRenderWithMalformedMjml()
{
$this->expectException(ProcessException::class);
$this->expectExceptionMessage('Malformed MJML. Check that your structure is correct and enclosed in <mjml> tags.');
$renderer = new BinaryRenderer(__DIR__ . '/../../node_modules/.bin/mjml');
$renderer->render('<mljm></mljm>');
}
}

View file

@ -1,53 +0,0 @@
<!-- FILE: undefined -->
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}</style><!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]--><!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}</style><style media="screen and (min-width:480px)">.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}</style><style type="text/css"></style><style type="text/css"></style></head><body style="word-spacing:normal;"><div><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tbody><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">Hello world</div></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>

View file

@ -1,9 +0,0 @@
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello world</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View file

@ -1,11 +0,0 @@
<?php
namespace Qferrer\Tests\Mjml;
trait ResourcesTrait
{
private function loadResource($filename)
{
return file_get_contents(__DIR__ . '/Resources/' . $filename);
}
}