Skip to content
Merged
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
17 changes: 8 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@
# Mirrors php-builds approach: compiles PHP from source then builds extension
#
# Build args:
# VERSION - PHP version (83, 84, 85) - defaults to 85 (PHP 8.5)
# PHP_VERSION - PHP version (8.3, 8.4, 8.5) - defaults to 8.5
#
ARG VERSION=85
ARG PHP_VERSION=8.5

FROM ubuntu:24.04

ARG VERSION
ARG PHP_VERSION
ENV DEBIAN_FRONTEND=noninteractive
ENV PHP_VERSION=${VERSION}

LABEL maintainer="Signalforge Team"
LABEL description="PHP with signalforge_http extension"
LABEL description="PHP ${PHP_VERSION} with signalforge_http extension"
LABEL php.version="${PHP_VERSION}"

# Install build dependencies
Expand Down Expand Up @@ -45,8 +44,8 @@ RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \

# Clone and build PHP from source
WORKDIR /tmp
RUN MAJOR=$(echo ${PHP_VERSION} | cut -c1); \
MINOR=$(echo ${PHP_VERSION} | cut -c2); \
RUN MAJOR=$(echo ${PHP_VERSION} | cut -d. -f1); \
MINOR=$(echo ${PHP_VERSION} | cut -d. -f2); \
PHP_BRANCH="PHP-${MAJOR}.${MINOR}"; \
git clone --depth 1 --branch ${PHP_BRANCH} --quiet https://github.com/php/php-src.git

Expand All @@ -71,8 +70,8 @@ RUN ./configure --quiet \
--enable-bcmath \
--with-readline

RUN make -j$(nproc) -s
RUN make install -s
RUN make -j$(nproc)
RUN make install

# Create extension config directory
RUN mkdir -p /usr/local/etc/php/conf.d
Expand Down
93 changes: 80 additions & 13 deletions Dockerfile.valgrind
Original file line number Diff line number Diff line change
@@ -1,32 +1,99 @@
# Dockerfile.valgrind - For memory leak detection
# Build with debug symbols and no optimization for meaningful Valgrind output
# Mirrors CI environment: Ubuntu 24.04 + PHP built from source

ARG PHP_VERSION=8.4
ARG PHP_VERSION=8.5

FROM php:${PHP_VERSION}-cli-alpine
FROM ubuntu:24.04

ARG PHP_VERSION
ENV DEBIAN_FRONTEND=noninteractive

LABEL maintainer="Signalforge Team"
LABEL description="PHP ${PHP_VERSION} with signalforge_http extension (Debug + Valgrind)"
LABEL php.version="${PHP_VERSION}"

# Install build dependencies AND valgrind
RUN apk add --no-cache \
autoconf g++ gcc make pkgconf re2c linux-headers valgrind ${PHPIZE_DEPS}
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
git \
gcc \
g++ \
make \
autoconf \
automake \
libtool \
pkg-config \
re2c \
bison \
wget \
valgrind \
libxml2-dev \
libssl-dev \
libcurl4-openssl-dev \
libzip-dev \
libonig-dev \
libsqlite3-dev \
libpq-dev \
libreadline-dev \
libpcre2-dev \
libsodium-dev \
zlib1g-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /build
# Clone and build PHP from source (with debug symbols)
WORKDIR /tmp
RUN MAJOR=$(echo ${PHP_VERSION} | cut -d. -f1); \
MINOR=$(echo ${PHP_VERSION} | cut -d. -f2); \
PHP_BRANCH="PHP-${MAJOR}.${MINOR}"; \
git clone --depth 1 --branch ${PHP_BRANCH} --quiet https://github.com/php/php-src.git

WORKDIR /tmp/php-src
RUN ./buildconf --force > /dev/null

# Configure PHP (same as Dockerfile, debug symbols will come from CFLAGS)
RUN CFLAGS="-g" ./configure --quiet \
--prefix=/usr/local \
--with-config-file-path=/usr/local/etc/php \
--with-config-file-scan-dir=/usr/local/etc/php/conf.d \
--with-curl \
--with-openssl \
--with-zip \
--with-zlib \
--enable-mbstring \
--enable-opcache \
--with-pdo-mysql \
--with-pdo-pgsql \
--with-mysqli \
--enable-sockets \
--enable-pcntl \
--enable-bcmath \
--with-readline

RUN make -j$(nproc)
RUN make install

# Create extension config directory
RUN mkdir -p /usr/local/etc/php/conf.d

# Copy extension source
# Copy and build http extension with debug symbols (no optimization for accurate valgrind)
WORKDIR /build
COPY . /build

# Build the extension with debug symbols (critical for Valgrind)
RUN phpize \
RUN /usr/local/bin/phpize \
&& CFLAGS="-g -O0" ./configure --enable-signalforge_http \
&& make \
&& make install \
&& docker-php-ext-enable signalforge_http
&& make -j$(nproc) \
&& make install

# Get run-tests.php from PHP source
RUN wget -q -O /opt/run-tests.php https://raw.githubusercontent.com/php/php-src/master/run-tests.php
# Enable extension
RUN echo "extension=signalforge_http.so" > /usr/local/etc/php/conf.d/signalforge_http.ini

# Configure PHP for Valgrind
RUN echo "zend.assertions=1" >> /usr/local/etc/php/conf.d/valgrind.ini

# Get run-tests.php from PHP source
RUN wget -q -O /opt/run-tests.php https://raw.githubusercontent.com/php/php-src/master/run-tests.php

# Verify extension is loaded
RUN php -m | grep signalforge_http

Expand Down
46 changes: 23 additions & 23 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
# Signalforge HTTP Extension - Docker-based Build
IMAGE_NAME = signalforge-http
VERSION ?= 85
PHP_VERSION ?= 8.5

.PHONY: docker-build docker-test docker-example docker-shell docker-clean valgrind-test valgrind-docker ci-test-all test-version build-push help

help:
@echo "Signalforge HTTP Extension"
@echo ""
@echo "Usage:"
@echo " make docker-build - Build Docker image with extension (default: PHP 8.5, VERSION=85)"
@echo " make docker-test - Run tests in Docker (default: PHP 8.5, VERSION=85)"
@echo " make docker-build - Build Docker image with extension (default: PHP 8.5)"
@echo " make docker-test - Run tests in Docker (default: PHP 8.5)"
@echo " make docker-example - Run example in Docker"
@echo " make docker-shell - Interactive shell in Docker"
@echo " make docker-clean - Remove Docker images"
@echo " make ci-test-all - Build and test PHP 8.3, 8.4, and 8.5"
@echo " make test-version - Run tests using ghcr.io image (VERSION=85)"
@echo " make test-version - Run tests using locally built extension"
@echo " make valgrind-docker - Run Valgrind memory check in Docker"
@echo " make valgrind-test - Run tests with local Valgrind"
@echo ""
@echo "Supported versions: VERSION=83 (PHP 8.3), VERSION=84 (PHP 8.4), VERSION=85 (PHP 8.5)"
@echo "Example: make docker-build VERSION=84"
@echo "Supported versions: PHP_VERSION=8.3, PHP_VERSION=8.4, PHP_VERSION=8.5 (default)"
@echo "Example: make docker-build PHP_VERSION=8.4"

docker-build:
docker build --build-arg VERSION=$(VERSION) -t $(IMAGE_NAME):$(VERSION) -t $(IMAGE_NAME):latest .
docker build --platform linux/amd64 --build-arg PHP_VERSION=$(PHP_VERSION) -t $(IMAGE_NAME):$(PHP_VERSION) -t $(IMAGE_NAME):latest .

docker-test:
docker run --rm -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):$(VERSION) php /opt/run-tests.php /ext/tests/
docker run --rm --platform linux/amd64 -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):$(PHP_VERSION) php /opt/run-tests.php /ext/tests/

docker-example:
docker run --rm -v $(PWD)/examples:/ext/examples $(IMAGE_NAME):$(VERSION) php /ext/examples/basic.php
docker run --rm --platform linux/amd64 -v $(PWD)/examples:/ext/examples $(IMAGE_NAME):$(PHP_VERSION) php /ext/examples/basic.php

docker-shell:
docker run --rm -it -v $(PWD):/ext $(IMAGE_NAME):$(VERSION) sh
docker run --rm --platform linux/amd64 -it -v $(PWD):/ext $(IMAGE_NAME):$(PHP_VERSION) bash

docker-clean:
docker rmi $(IMAGE_NAME) 2>/dev/null || true
Expand All @@ -55,33 +55,33 @@ valgrind-test:
# CI: Build and test all PHP versions
ci-test-all:
@echo "Building and testing PHP 8.3..."
docker build --build-arg VERSION=83 -t $(IMAGE_NAME):83 .
docker run --rm -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):83 php /opt/run-tests.php -q /ext/tests/
docker build --platform linux/amd64 --build-arg PHP_VERSION=8.3 -t $(IMAGE_NAME):8.3 .
docker run --rm --platform linux/amd64 -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):8.3 php /opt/run-tests.php -q /ext/tests/
@echo ""
@echo "Building and testing PHP 8.4..."
docker build --build-arg VERSION=84 -t $(IMAGE_NAME):84 .
docker run --rm -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):84 php /opt/run-tests.php -q /ext/tests/
docker build --platform linux/amd64 --build-arg PHP_VERSION=8.4 -t $(IMAGE_NAME):8.4 .
docker run --rm --platform linux/amd64 -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):8.4 php /opt/run-tests.php -q /ext/tests/
@echo ""
@echo "Building and testing PHP 8.5..."
docker build --build-arg VERSION=85 -t $(IMAGE_NAME):85 .
docker run --rm -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):85 php /opt/run-tests.php -q /ext/tests/
docker build --platform linux/amd64 --build-arg PHP_VERSION=8.5 -t $(IMAGE_NAME):8.5 .
docker run --rm --platform linux/amd64 -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):8.5 php /opt/run-tests.php -q /ext/tests/
@echo ""
@echo "All PHP versions tested successfully!"

# Valgrind: Run memory check in Docker
valgrind-docker:
@echo "Building Valgrind Docker image..."
docker build -f Dockerfile.valgrind -t $(IMAGE_NAME):valgrind .
docker build --platform linux/amd64 --build-arg PHP_VERSION=$(PHP_VERSION) -f Dockerfile.valgrind -t $(IMAGE_NAME):valgrind .
@echo ""
@echo "Running Valgrind memory check..."
docker run --rm -v $(PWD)/tests:/ext/tests -v $(PWD):/output $(IMAGE_NAME):valgrind \
sh -c 'valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 \
docker run --rm --platform linux/amd64 -v $(PWD)/tests:/ext/tests -v $(PWD):/output $(IMAGE_NAME):valgrind \
bash -c 'valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 \
--track-origins=yes --log-file=/output/valgrind-output.txt \
php /opt/run-tests.php -q /ext/tests/ && \
cat /output/valgrind-output.txt | grep -E "ERROR SUMMARY|LEAK SUMMARY" -A 5'

# Run tests using pre-built ghcr.io image with configurable PHP version
# Run tests using locally built extension with configurable PHP version
test-version:
@echo "Running tests with PHP 8.$(VERSION) from ghcr.io..."
docker run --rm -v $(PWD):/ext ghcr.io/thesignalforge/signalforge:php$(VERSION) \
php /usr/local/lib/php/build/run-tests.php /ext/tests/
@echo "Running tests with locally built PHP $(PHP_VERSION) extension (linux/amd64)..."
docker run --rm --platform linux/amd64 -v $(PWD)/tests:/ext/tests $(IMAGE_NAME):$(PHP_VERSION) \
php /opt/run-tests.php /ext/tests/
86 changes: 29 additions & 57 deletions src/request.c
Original file line number Diff line number Diff line change
Expand Up @@ -831,56 +831,6 @@ PHP_METHOD(Signalforge_Http_Request, create)
RETURN_THROWS();
}

/* Extract URI string from parameter */
if (Z_TYPE_P(uri_param) == IS_STRING) {
uri_str = Z_STR_P(uri_param);
} else if (Z_TYPE_P(uri_param) == IS_OBJECT && instanceof_function(Z_OBJCE_P(uri_param), signalforge_uri_ce)) {
/* Uri object - convert to string */
signalforge_uri_object *uri_obj = Z_SIGNALFORGE_URI_P(uri_param);
smart_str buf = {0};

if (uri_obj->scheme && ZSTR_LEN(uri_obj->scheme) > 0) {
smart_str_append(&buf, uri_obj->scheme);
smart_str_appendl(&buf, "://", 3);
}
if (uri_obj->host && ZSTR_LEN(uri_obj->host) > 0) {
if (uri_obj->user && ZSTR_LEN(uri_obj->user) > 0) {
smart_str_append(&buf, uri_obj->user);
if (uri_obj->pass && ZSTR_LEN(uri_obj->pass) > 0) {
smart_str_appendc(&buf, ':');
smart_str_append(&buf, uri_obj->pass);
}
smart_str_appendc(&buf, '@');
}
smart_str_append(&buf, uri_obj->host);
if (uri_obj->port != SIGNALFORGE_PORT_UNSET &&
!signalforge_is_standard_port(uri_obj->scheme, uri_obj->port)) {
smart_str_appendc(&buf, ':');
smart_str_append_long(&buf, uri_obj->port);
}
}
if (uri_obj->path && ZSTR_LEN(uri_obj->path) > 0) {
smart_str_append(&buf, uri_obj->path);
} else if (!uri_obj->host || ZSTR_LEN(uri_obj->host) == 0) {
smart_str_appendc(&buf, '/');
}
if (uri_obj->query && ZSTR_LEN(uri_obj->query) > 0) {
smart_str_appendc(&buf, '?');
smart_str_append(&buf, uri_obj->query);
}
if (uri_obj->fragment && ZSTR_LEN(uri_obj->fragment) > 0) {
smart_str_appendc(&buf, '#');
smart_str_append(&buf, uri_obj->fragment);
}

smart_str_0(&buf);
uri_str = buf.s ? buf.s : zend_empty_string;
} else {
zend_throw_exception(spl_ce_InvalidArgumentException,
"URI must be a string or Uri object", 0);
RETURN_THROWS();
}

/* Create new instance */
object_init_ex(return_value, signalforge_request_ce);
intern = Z_SIGNALFORGE_REQUEST_P(return_value);
Expand All @@ -902,15 +852,37 @@ PHP_METHOD(Signalforge_Http_Request, create)
ALLOC_HASHTABLE(intern->ht_attributes);
zend_hash_init(intern->ht_attributes, 8, NULL, ZVAL_PTR_DTOR, 0);

/* Set the URI */
ZVAL_STR_COPY(&intern->zv_uri, uri_str);
intern->request_uri = Z_STRVAL(intern->zv_uri);
intern->request_uri_len = Z_STRLEN(intern->zv_uri);
/* Set the URI - handle ownership correctly */
if (Z_TYPE_P(uri_param) == IS_STRING) {
/* Borrowed from parameter - must copy */
ZVAL_STR_COPY(&intern->zv_uri, Z_STR_P(uri_param));
} else if (Z_TYPE_P(uri_param) == IS_OBJECT && instanceof_function(Z_OBJCE_P(uri_param), signalforge_uri_ce)) {
/* Uri object - call __toString() and transfer ownership */
zval uri_string_zv;
zend_call_method_with_0_params(Z_OBJ_P(uri_param), Z_OBJCE_P(uri_param),
NULL, "__tostring", &uri_string_zv);

if (EG(exception)) {
RETURN_THROWS();
}

if (Z_TYPE(uri_string_zv) != IS_STRING) {
zval_ptr_dtor(&uri_string_zv);
zend_throw_exception(spl_ce_RuntimeException,
"Uri::__toString() must return a string", 0);
RETURN_THROWS();
}

/* Release temporary uri_str if it was created from Uri object conversion */
if (Z_TYPE_P(uri_param) == IS_OBJECT && instanceof_function(Z_OBJCE_P(uri_param), signalforge_uri_ce)) {
zend_string_release(uri_str);
/* Transfer ownership from temporary zval to object */
ZVAL_COPY_VALUE(&intern->zv_uri, &uri_string_zv);
ZVAL_UNDEF(&uri_string_zv); /* Mark as transferred, no double-free */
} else {
zend_throw_exception(spl_ce_InvalidArgumentException,
"URI must be a string or Uri object", 0);
RETURN_THROWS();
}
intern->request_uri = Z_STRVAL(intern->zv_uri);
intern->request_uri_len = Z_STRLEN(intern->zv_uri);

/* Set query string if present in URI */
const char *query_pos = strchr(Z_STRVAL(intern->zv_uri), '?');
Expand Down
9 changes: 7 additions & 2 deletions src/response.c
Original file line number Diff line number Diff line change
Expand Up @@ -636,9 +636,14 @@ PHP_METHOD(Signalforge_Http_Response, getHeaderLine)
} else if (Z_TYPE_P(val) == IS_STRING) {
smart_str_appendl(&str, Z_STRVAL_P(val), Z_STRLEN_P(val));
}

smart_str_0(&str);
RETURN_STR(str.s);
/* Handle edge case where header exists but is empty */
if (str.s) {
RETURN_STR(str.s);
} else {
RETURN_EMPTY_STRING();
}
}
/* }}} */

Expand Down
Loading