Skip to content

Commit 773e442

Browse files
committed
feat: PHP bindings
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent dcd2835 commit 773e442

File tree

10 files changed

+405
-0
lines changed

10 files changed

+405
-0
lines changed

.github/workflows/build.yml

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,135 @@ jobs:
531531
DEFAULT_CROSS_BUILD_ENV_URL: "https://github.com/pyodide/pyodide/releases/download/0.28.0a3/xbuildenv-0.28.0a3.tar.bz2"
532532
RUSTFLAGS: "-C link-arg=-sSIDE_MODULE=2 -Z link-native-libraries=no -Z emscripten-wasm-eh"
533533

534+
test-php:
535+
strategy:
536+
fail-fast: false
537+
matrix:
538+
os: [ubuntu-22.04, macos-13]
539+
php-version: ["8.2", "8.3", "8.4"]
540+
clang: ["20"]
541+
542+
name: PHP ${{ matrix.php-version }} on ${{ matrix.os }}
543+
runs-on: ${{ matrix.os }}
544+
env:
545+
CARGO_TERM_COLOR: always
546+
steps:
547+
- uses: actions/checkout@v4
548+
549+
- uses: dtolnay/rust-toolchain@stable
550+
551+
- name: Cache cargo dependencies
552+
uses: Swatinem/rust-cache@v2
553+
with:
554+
workspaces: bindings/php
555+
556+
- name: Cache LLVM and Clang
557+
id: cache-llvm
558+
uses: actions/cache@v4
559+
if: matrix.os == 'ubuntu-22.04'
560+
with:
561+
path: ${{ runner.temp }}/llvm-${{ matrix.clang }}
562+
key: ${{ matrix.os }}-llvm-${{ matrix.clang }}
563+
564+
- name: Setup LLVM & Clang
565+
id: clang
566+
uses: KyleMayes/install-llvm-action@v2
567+
if: matrix.os == 'ubuntu-22.04'
568+
with:
569+
version: ${{ matrix.clang }}
570+
directory: ${{ runner.temp }}/llvm-${{ matrix.clang }}
571+
cached: ${{ steps.cache-llvm.outputs.cache-hit }}
572+
573+
- name: Configure Clang
574+
if: matrix.os == 'ubuntu-22.04'
575+
run: |
576+
echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.clang }}/lib" >> $GITHUB_ENV
577+
echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV
578+
echo "LLVM_CONFIG_PATH=${{ runner.temp }}/llvm-${{ matrix.clang }}/bin/llvm-config" >> $GITHUB_ENV
579+
580+
- uses: shivammathur/setup-php@v2
581+
with:
582+
php-version: ${{ matrix.php-version }}
583+
extensions: mbstring
584+
coverage: none
585+
586+
- name: Build PHP extension
587+
run: |
588+
# Export PHP configuration for ext-php-rs
589+
export PHP_CONFIG=$(which php-config)
590+
591+
cargo build --release
592+
593+
# Get PHP extension directory
594+
EXT_DIR=$(php -r "echo ini_get('extension_dir');")
595+
596+
# Find the built library - ext-php-rs names it differently on different platforms
597+
if [[ "${{ matrix.os }}" == "macos-13" ]]; then
598+
# On macOS, look for .dylib files
599+
BUILT_LIB=$(find target/release -name "libcss_inline_php.dylib" -o -name "css_inline_php.dylib" | head -1)
600+
if [[ -z "$BUILT_LIB" ]]; then
601+
# Fallback: any .dylib file
602+
BUILT_LIB=$(find target/release -name "*.dylib" | head -1)
603+
fi
604+
sudo cp "$BUILT_LIB" "$EXT_DIR/css_inline.so"
605+
else
606+
# On Linux, look for .so files
607+
BUILT_LIB=$(find target/release -name "libcss_inline_php.so" -o -name "css_inline_php.so" | head -1)
608+
if [[ -z "$BUILT_LIB" ]]; then
609+
# Fallback: any .so file
610+
BUILT_LIB=$(find target/release -name "*.so" | head -1)
611+
fi
612+
sudo cp "$BUILT_LIB" "$EXT_DIR/css_inline.so"
613+
fi
614+
615+
echo "Built library: $BUILT_LIB"
616+
echo "Installed to: $EXT_DIR/css_inline.so"
617+
618+
# Verify the file exists and has correct permissions
619+
ls -la "$EXT_DIR/css_inline.so"
620+
working-directory: ./bindings/php
621+
shell: bash
622+
623+
- name: Enable and verify extension
624+
run: |
625+
# Create ini file to load extension
626+
if [[ "${{ matrix.os }}" == "macos-13" ]]; then
627+
# On macOS, find the additional ini directory
628+
PHP_INI_DIR=$(php -i | grep "Scan this dir for additional .ini files" | cut -d' ' -f9 | tr -d ' ')
629+
if [[ -z "$PHP_INI_DIR" || "$PHP_INI_DIR" == "(none)" ]]; then
630+
# If no scan dir, use the main php.ini location
631+
PHP_INI=$(php -i | grep "Loaded Configuration File" | cut -d' ' -f9)
632+
PHP_INI_DIR=$(dirname "$PHP_INI")/conf.d
633+
sudo mkdir -p "$PHP_INI_DIR"
634+
fi
635+
echo "extension=css_inline" | sudo tee "$PHP_INI_DIR/99-css_inline.ini"
636+
else
637+
echo "extension=css_inline" | sudo tee /etc/php/${{ matrix.php-version }}/cli/conf.d/99-css_inline.ini
638+
fi
639+
640+
# Verify extension is loaded
641+
php -m | grep -i css_inline || (
642+
echo "Extension failed to load. Debugging info:"
643+
echo "PHP Version:"
644+
php -v
645+
echo "Extension dir contents:"
646+
ls -la $(php -r "echo ini_get('extension_dir');")
647+
echo "PHP info grep for css_inline:"
648+
php -i | grep -i css_inline || true
649+
echo "Try loading directly:"
650+
php -d "extension=$(php -r 'echo ini_get("extension_dir");')/css_inline.so" -m | grep -i css_inline || true
651+
exit 1
652+
)
653+
shell: bash
654+
655+
- name: Install dependencies
656+
run: composer install --no-interaction --prefer-dist
657+
working-directory: ./bindings/php
658+
659+
- name: Run tests
660+
run: composer test
661+
working-directory: ./bindings/php
662+
534663
test-ruby:
535664
strategy:
536665
fail-fast: false

bindings/php/.cargo/config.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[target.x86_64-unknown-linux-gnu]
2+
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
3+
4+
[target.x86_64-apple-darwin]
5+
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
6+
7+
[target.aarch64-apple-darwin]
8+
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
9+
10+
[target.x86_64-pc-windows-msvc]
11+
linker = "rust-lld"
12+
rustflags = ["-C", "link-arg=/FORCE"]

bindings/php/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/vendor/
2+
/composer.lock
3+
/.phpunit.cache/

bindings/php/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "css_inline"
3+
version = "0.15.0"
4+
edition = "2024"
5+
authors = ["Dmitry Dygalo <[email protected]>"]
6+
7+
[lib]
8+
name = "css_inline_php"
9+
crate-type = ["cdylib"]
10+
11+
[dependencies]
12+
ext-php-rs = "0.14"
13+
14+
[dependencies.css-inline]
15+
path = "../../css-inline"
16+
version = "*"
17+
default-features = false
18+
features = ["http", "file", "stylesheet-cache"]

bindings/php/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# css_inline
2+
3+
[<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)
4+
[<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)
5+
[<img alt="gitter" src="https://img.shields.io/gitter/room/Stranger6667/css-inline?style=flat-square" height="20">](https://gitter.im/Stranger6667/css-inline)
6+
7+
`css_inline` is a high-performance library for inlining CSS into HTML 'style' attributes.

bindings/php/composer.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "css-inline/php",
3+
"description": "High-performance CSS inlining for PHP",
4+
"type": "library",
5+
"license": "MIT",
6+
"require": {
7+
"php": ">=8.1",
8+
"ext-css_inline": "*"
9+
},
10+
"require-dev": {
11+
"phpunit/phpunit": "^10.5"
12+
},
13+
"autoload-dev": {
14+
"psr-4": {
15+
"CssInline\\Tests\\": "tests/"
16+
}
17+
},
18+
"scripts": {
19+
"test": "phpunit"
20+
},
21+
"config": {
22+
"sort-packages": true,
23+
"optimize-autoloader": true
24+
}
25+
}

bindings/php/phpunit.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
4+
colors="true"
5+
stopOnFailure="false">
6+
<testsuites>
7+
<testsuite name="CssInline Test Suite">
8+
<directory>tests</directory>
9+
</testsuite>
10+
</testsuites>
11+
</phpunit>

bindings/php/src/lib.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use std::fmt::Display;
2+
3+
use ext_php_rs::{exception::PhpException, prelude::*, zend::ce};
4+
5+
#[php_class]
6+
#[php(name = "CssInline\\InlineError")]
7+
#[php(extends(ce = ce::exception, stub = "\\Exception"))]
8+
#[derive(Default)]
9+
pub struct InlineError;
10+
11+
fn from_error<E: Display>(error: E) -> PhpException {
12+
PhpException::from_class::<InlineError>(error.to_string())
13+
}
14+
15+
#[php_class]
16+
#[php(name = "CssInline\\CssInliner")]
17+
pub struct CssInliner {
18+
inner: css_inline::CSSInliner<'static>,
19+
}
20+
21+
#[php_impl]
22+
impl CssInliner {
23+
#[php(defaults(
24+
inline_style_tags = true,
25+
keep_style_tags = false,
26+
keep_link_tags = false,
27+
load_remote_stylesheets = true,
28+
))]
29+
#[php(optional = inline_style_tags)]
30+
pub fn __construct(
31+
inline_style_tags: bool,
32+
keep_style_tags: bool,
33+
keep_link_tags: bool,
34+
load_remote_stylesheets: bool,
35+
base_url: Option<String>,
36+
extra_css: Option<String>,
37+
) -> PhpResult<CssInliner> {
38+
let base_url = if let Some(url) = base_url {
39+
Some(css_inline::Url::parse(&url).map_err(from_error)?)
40+
} else {
41+
None
42+
};
43+
44+
let options = css_inline::InlineOptions {
45+
inline_style_tags,
46+
keep_style_tags,
47+
keep_link_tags,
48+
base_url,
49+
load_remote_stylesheets,
50+
extra_css: extra_css.map(Into::into),
51+
..Default::default()
52+
};
53+
54+
Ok(CssInliner {
55+
inner: css_inline::CSSInliner::new(options),
56+
})
57+
}
58+
59+
pub fn inline(&self, html: &str) -> PhpResult<String> {
60+
self.inner.inline(html).map_err(from_error)
61+
}
62+
63+
pub fn inline_fragment(&self, html: &str, css: &str) -> PhpResult<String> {
64+
self.inner.inline_fragment(html, css).map_err(from_error)
65+
}
66+
}
67+
68+
#[php_function]
69+
#[php(name = "CssInline\\inline")]
70+
pub fn inline(html: &str) -> PhpResult<String> {
71+
css_inline::inline(html).map_err(from_error)
72+
}
73+
74+
#[php_function]
75+
#[php(name = "CssInline\\inline_fragment")]
76+
pub fn inline_fragment(fragment: &str, css: &str) -> PhpResult<String> {
77+
css_inline::inline_fragment(fragment, css).map_err(from_error)
78+
}
79+
80+
#[php_module]
81+
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
82+
module
83+
.class::<InlineError>()
84+
.class::<CssInliner>()
85+
.function(wrap_function!(inline))
86+
.function(wrap_function!(inline_fragment))
87+
}

bindings/php/stubs/css_inline.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
// Stubs for css_inline
4+
5+
namespace CssInline {
6+
function inline(string $html): string {}
7+
8+
function inline_fragment(string $fragment, string $css): string {}
9+
10+
class InlineError extends \Exception {
11+
public function __construct() {}
12+
}
13+
14+
class CssInliner {
15+
public function inline(string $html): string {}
16+
17+
public function inlineFragment(string $html, string $css): string {}
18+
19+
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) {}
20+
}
21+
}

0 commit comments

Comments
 (0)