Skip to content

feat: PHP bindings #499

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,90 @@ jobs:
DEFAULT_CROSS_BUILD_ENV_URL: "https://github.com/pyodide/pyodide/releases/download/0.28.0a3/xbuildenv-0.28.0a3.tar.bz2"
RUSTFLAGS: "-C link-arg=-sSIDE_MODULE=2 -Z link-native-libraries=no -Z emscripten-wasm-eh"

test-php:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-13]
php-version: ["8.2", "8.3", "8.4"]
clang: ["20"]

name: PHP ${{ matrix.php-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable

- name: Cache LLVM and Clang
id: cache-llvm
uses: actions/cache@v4
if: matrix.os == 'ubuntu-22.04'
with:
path: ${{ runner.temp }}/llvm-${{ matrix.clang }}
key: ${{ matrix.os }}-llvm-${{ matrix.clang }}

- name: Setup LLVM & Clang
id: clang
uses: KyleMayes/install-llvm-action@v2
if: matrix.os == 'ubuntu-22.04'
with:
version: ${{ matrix.clang }}
directory: ${{ runner.temp }}/llvm-${{ matrix.clang }}
cached: ${{ steps.cache-llvm.outputs.cache-hit }}

- name: Configure Clang
if: matrix.os == 'ubuntu-22.04'
run: |
echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.clang }}/lib" >> $GITHUB_ENV
echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV
echo "LLVM_CONFIG_PATH=${{ runner.temp }}/llvm-${{ matrix.clang }}/bin/llvm-config" >> $GITHUB_ENV

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring
coverage: none

- name: Build PHP extension
run: |
export PHP_CONFIG=$(which php-config)

cargo build --release

EXT_DIR=$(php -r "echo ini_get('extension_dir');")

if [[ "${{ matrix.os }}" == "macos-13" ]]; then
BUILT_LIB=$(find target/release -name "libcss_inline_php.dylib" -o -name "css_inline_php.dylib" | head -1)
if [[ -z "$BUILT_LIB" ]]; then
BUILT_LIB=$(find target/release -name "*.dylib" | head -1)
fi
sudo cp "$BUILT_LIB" "$EXT_DIR/css_inline.so"
else
BUILT_LIB=$(find target/release -name "*.so" | head -1)
sudo cp "$BUILT_LIB" "$EXT_DIR/css_inline.so"
fi
working-directory: ./bindings/php
shell: bash

- name: Enable and verify extension
run: |
if [[ "${{ matrix.os }}" == "macos-13" ]]; then
PHP_INI_DIR=$(php -i | grep "Scan this dir for additional .ini files" | cut -d' ' -f9 | tr -d ' ')
echo "extension=css_inline" | sudo tee "$PHP_INI_DIR/99-css_inline.ini"
else
echo "extension=css_inline" | sudo tee /etc/php/${{ matrix.php-version }}/cli/conf.d/99-css_inline.ini
fi
shell: bash

- name: Install dependencies
run: composer install --no-interaction --prefer-dist
working-directory: ./bindings/php

- name: Run tests
run: composer test
working-directory: ./bindings/php

test-ruby:
strategy:
fail-fast: false
Expand Down
12 changes: 12 additions & 0 deletions bindings/php/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]

[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]

[target.aarch64-apple-darwin]
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]

[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
rustflags = ["-C", "link-arg=/FORCE"]
3 changes: 3 additions & 0 deletions bindings/php/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/vendor/
/composer.lock
/.phpunit.cache/
18 changes: 18 additions & 0 deletions bindings/php/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "css_inline"
version = "0.15.0"
edition = "2024"
authors = ["Dmitry Dygalo <[email protected]>"]

[lib]
name = "css_inline_php"
crate-type = ["cdylib"]

[dependencies]
ext-php-rs = "0.14.1"

[dependencies.css-inline]
path = "../../css-inline"
version = "*"
default-features = false
features = ["http", "file", "stylesheet-cache"]
24 changes: 24 additions & 0 deletions bindings/php/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# css_inline

[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/Stranger6667/css-inline/build.yml?style=flat-square&labelColor=555555&logo=github" height="20">](https://github.com/Stranger6667/css-inline/actions/workflows/build.yml)
[<img alt="codecov.io" src="https://img.shields.io/codecov/c/gh/Stranger6667/css-inline?logo=codecov&style=flat-square&token=tOzvV4kDY0" height="20">](https://app.codecov.io/github/Stranger6667/css-inline)
[<img alt="gitter" src="https://img.shields.io/gitter/room/Stranger6667/css-inline?style=flat-square" height="20">](https://gitter.im/Stranger6667/css-inline)

`css_inline` is a high-performance library for inlining CSS into HTML 'style' attributes.

## Performance

This library uses components from Mozilla's Servo project for CSS parsing and matching.
Performance benchmarks show significant speed improvements over other popular PHP CSS inlining libraries.

| | Size | `css_inline 0.15.0` | `css-to-inline-styles 2.3.0` | `emogrifier 7.3.0` |
|-------------------|---------|---------------------|------------------------------|-------------------------|
| Simple | 230 B | 5.99 µs | 28.06 µs (**4.68x**) | 137.85 µs (**23.01x**) |
| Realistic email 1 | 8.58 KB | 102.25 µs | 313.31 µs (**3.06x**) | 637.75 µs (**6.24x**) |
| Realistic email 2 | 4.3 KB | 71.98 µs | 655.43 µs (**9.10x**) | 2.32 ms (**32.21x**) |
| GitHub Page† | 1.81 MB | 163.80 ms | ERROR | ERROR |

† The GitHub page benchmark contains complex modern CSS that neither `css-to-inline-styles` nor `emogrifier` can process and didn't finish a single iteration in >10 minutes.

Please refer to the `benchmarks/InlineBench.php` file to review the benchmark code.
The results displayed above were measured using stable `rustc 1.88` on PHP `8.4.10`.
59 changes: 59 additions & 0 deletions bindings/php/benchmarks/InlineBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace CssInline\Benchmarks;

use PhpBench\Benchmark\Metadata\Annotations\ParamProviders;
use CssInline;
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;
use Pelago\Emogrifier\CssInliner;

class InlineBench
{
private CssToInlineStyles $cssToInlineStyles;

public function __construct()
{
$this->cssToInlineStyles = new CssToInlineStyles();
ini_set('pcre.backtrack_limit', '10000000');
ini_set('pcre.recursion_limit', '10000000');
ini_set('memory_limit', '2048M');
}

/**
* @ParamProviders("provideBenchmarkCases")
*/
public function benchCssInline(array $params): void
{
\CssInline\inline($params['html']);
}

/**
* @ParamProviders("provideBenchmarkCases")
*/
public function benchCssToInlineStyles(array $params): void
{
$this->cssToInlineStyles->convert($params['html']);
}

/**
* @ParamProviders("provideBenchmarkCases")
*/
public function benchEmogrifier(array $params): void
{
CssInliner::fromHtml($params['html'])->inlineCss()->render();
}


public function provideBenchmarkCases(): \Generator
{
$jsonPath = __DIR__ . '/../../../benchmarks/benchmarks.json';
$json = file_get_contents($jsonPath);
$benchmarks = json_decode($json, true);

foreach ($benchmarks as $benchmark) {
yield $benchmark['name'] => [
'html' => $benchmark['html']
];
}
}
}
30 changes: 30 additions & 0 deletions bindings/php/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "css-inline/php",
"description": "High-performance library for inlining CSS into HTML 'style' attributes",
"type": "library",
"license": "MIT",
"require": {
"php": ">=8.2",
"ext-css_inline": "*"
},
"require-dev": {
"pelago/emogrifier": "^7.3",
"phpbench/phpbench": "^1.4",
"phpunit/phpunit": "^10.5",
"tijsverkoyen/css-to-inline-styles": "^2.3"
},
"autoload-dev": {
"psr-4": {
"CssInline\\Tests\\": "tests/CssInlineTest"
}
},
"scripts": {
"test": "phpunit",
"bench": "phpbench run --report=default --iterations=10 --revs=100"
},
"config": {
"sort-packages": true,
"optimize-autoloader": true,
"process-timeout": 0
}
}
8 changes: 8 additions & 0 deletions bindings/php/phpbench.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"runner.bootstrap": "vendor/autoload.php",
"runner.path": "benchmarks",
"runner.php_config": {
"extension": "target/release/libcss_inline_php.so"
},
"runner.timeout": 3600
}
11 changes: 11 additions & 0 deletions bindings/php/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
stopOnFailure="false">
<testsuites>
<testsuite name="CssInline Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
89 changes: 89 additions & 0 deletions bindings/php/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use std::fmt::Display;

use ext_php_rs::{exception::PhpException, prelude::*, zend::ce};

#[php_class]
#[php(name = "CssInline\\InlineError")]
#[php(extends(ce = ce::exception, stub = "\\Exception"))]
#[derive(Default)]
pub struct InlineError;

fn from_error<E: Display>(error: E) -> PhpException {
PhpException::from_class::<InlineError>(error.to_string())
}

#[php_class]
#[php(name = "CssInline\\CssInliner")]
pub struct CssInliner {
inner: css_inline::CSSInliner<'static>,
}

#[php_impl]
impl CssInliner {
#[php(defaults(
inline_style_tags = true,
keep_style_tags = false,
keep_link_tags = false,
load_remote_stylesheets = true,
base_url = None,
extra_css = None,
))]
#[php(optional = inline_style_tags)]
pub fn __construct(
inline_style_tags: bool,
keep_style_tags: bool,
keep_link_tags: bool,
load_remote_stylesheets: bool,
base_url: Option<String>,
extra_css: Option<String>,
) -> PhpResult<CssInliner> {
let base_url = if let Some(url) = base_url {
Some(css_inline::Url::parse(&url).map_err(from_error)?)
} else {
None
};

let options = css_inline::InlineOptions {
inline_style_tags,
keep_style_tags,
keep_link_tags,
base_url,
load_remote_stylesheets,
extra_css: extra_css.map(Into::into),
..Default::default()
};

Ok(CssInliner {
inner: css_inline::CSSInliner::new(options),
})
}

pub fn inline(&self, html: &str) -> PhpResult<String> {
self.inner.inline(html).map_err(from_error)
}

pub fn inline_fragment(&self, html: &str, css: &str) -> PhpResult<String> {
self.inner.inline_fragment(html, css).map_err(from_error)
}
}

#[php_function]
#[php(name = "CssInline\\inline")]
pub fn inline(html: &str) -> PhpResult<String> {
css_inline::inline(html).map_err(from_error)
}

#[php_function]
#[php(name = "CssInline\\inline_fragment")]
pub fn inline_fragment(fragment: &str, css: &str) -> PhpResult<String> {
css_inline::inline_fragment(fragment, css).map_err(from_error)
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.class::<InlineError>()
.class::<CssInliner>()
.function(wrap_function!(inline))
.function(wrap_function!(inline_fragment))
}
21 changes: 21 additions & 0 deletions bindings/php/stubs/css_inline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

// Stubs for css_inline

namespace CssInline {
function inline(string $html): string {}

function inline_fragment(string $fragment, string $css): string {}

class InlineError extends \Exception {
public function __construct() {}
}

class CssInliner {
public function inline(string $html): string {}

public function inlineFragment(string $html, string $css): string {}

public function __construct(?bool $inline_style_tags, ?bool $keep_style_tags, ?bool $keep_link_tags, ?bool $load_remote_stylesheets, ?string $base_url, ?string $extra_css) {}
}
}
Loading
Loading