diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7a407f1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,56 @@
+# Stubs
+*_arginfo.h
+vendor/
+
+# Build products
+*.so
+*.lo
+*.la
+*.dep
+
+# Build directories
+.libs/
+build/
+
+# Configuration files
+config.h
+config.h.in
+config.log
+config.nice
+config.status
+configure
+configure.ac
+
+# Makefiles
+Makefile
+Makefile.fragments
+Makefile.objects
+
+# Autom4te cache
+autom4te.cache/
+
+# Libtool files
+libtool
+
+# Test files
+run-tests.php
+
+# IDE/Editor files
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# macOS
+.DS_Store
+
+# Editor backup files
+*~
+
+# PHPT test artifacts
+tests/phpt/*.diff
+tests/phpt/*.exp
+tests/phpt/*.log
+tests/phpt/*.out
+tests/phpt/*.php
+tests/phpt/*.sh
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 0000000..0975e07
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,8 @@
+# Development
+
+## Dependencies
+
+- PHP 8.2+
+- [Composer](https://getcomposer.org)
+- [Task](https://taskfile.dev)
+- [Reflex](https://github.com/cespare/reflex)
diff --git a/README.md b/README.md
index bcc9a9f..f61184d 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,92 @@
-# template-ext
-Modern PHP extension template - PestPHP + C unit tests ๐งช
+# Template Extension
+
+**Template Extension** is a PHP extension template with string to_snake_case conversion functionality.
+
+## Features
+
+- `to_snake(string $input)` - Converts strings to snake_case format
+
+## Supported Conversions
+
+- camelCase โ snake_case
+- PascalCase โ snake_case
+- kebab-case โ snake_case
+- Space separated โ snake_case
+- Already snake_case โ snake_case (unchanged)
+
+## Installation
+
+```bash
+# Build and install the extension
+phpize && ./configure --enable-template-ext
+make
+make install
+```
+
+## Usage
+
+```php
+
+
+
+
+ tests/php/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/project.yml b/project.yml
new file mode 100644
index 0000000..227ee3d
--- /dev/null
+++ b/project.yml
@@ -0,0 +1,159 @@
+# =========================================================================
+# Ceedling - Test-Centered Build System for C
+# ThrowTheSwitch.org
+# Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
+# SPDX-License-Identifier: MIT
+# =========================================================================
+
+---
+:project:
+ :name: "Template Extension"
+
+ # how to use ceedling. If you're not sure, leave this as `gem` and `?`
+ :which_ceedling: gem
+ :ceedling_version: 1.1.2
+
+ # optional features. If you don't need them, keep them turned off for performance
+ :use_mocks: FALSE
+ :use_test_preprocessor: :all # options are :none, :mocks, :tests, or :all
+ :use_backtrace: :simple # options are :none, :simple, or :gdb
+
+ # tweak the way ceedling handles automatic tasks
+ :build_root: build
+ :test_file_prefix: test_
+ :default_tasks:
+ - test:all
+
+ # performance options. If your tools start giving mysterious errors, consider
+ # dropping this to 1 to force single-tasking
+ :test_threads: 8
+ :compile_threads: 8
+
+ # enable release build (more details in release_build section below)
+ :release_build: FALSE
+
+# Specify where to find mixins and any that should be enabled automatically
+:mixins:
+ :enabled: []
+ :load_paths: []
+
+# further details to configure the way Ceedling handles test code
+:test_build:
+ :use_assembly: FALSE
+
+# further details to configure the way Ceedling handles release code
+:release_build:
+ :output: template_ext.out
+ :use_assembly: FALSE
+ :artifacts: []
+
+# Plugins are optional Ceedling features which can be enabled. Ceedling supports
+# a variety of plugins which may effect the way things are compiled, reported,
+# or may provide new command options. Refer to the readme in each plugin for
+# details on how to use it.
+:plugins:
+ :load_paths: []
+ :enabled:
+ - module_generator # handy for quickly creating source, header, and test templates
+ - report_tests_pretty_stdout
+
+# Specify which reports you'd like from the log factory
+:report_tests_log_factory:
+ :reports:
+ - json
+ - junit
+ - cppunit
+ - html
+
+# override the default extensions for your system and toolchain
+:extension:
+ :executable: .out
+
+# This is where Ceedling should look for your source and test files.
+:paths:
+ :test:
+ - +:tests/c/**
+ - -:tests/c/support
+ :source:
+ - template_ext.c
+ :include:
+ - template_ext.h
+ :support:
+ - tests/c/support
+ :libraries: []
+
+:files:
+ :test: []
+ :source: []
+
+:defines:
+ :test:
+ - TEST
+ - TESTING
+ :release: []
+ :use_test_definition: FALSE
+
+:flags:
+ :release:
+ :compile:
+ - -Wall
+ - --O2
+ :test:
+ :compile:
+ '*':
+ - -I$(php-config --include-dir)
+ - -I$(php-config --include-dir)/main
+ - -I$(php-config --include-dir)/TSRM
+ - -I$(php-config --include-dir)/Zend
+ - -I$(php-config --include-dir)/ext
+ - -I$(php-config --include-dir)/ext/date/lib
+ - -D_GNU_SOURCE
+ - -Wno-implicit-function-declaration
+
+:cmock:
+ :plugins:
+ - :ignore
+ - :callback
+ :verbosity: 2
+ :when_no_prototypes: :warn
+ :strippables: []
+ :attributes:
+ - __ramfunc
+ - __irq
+ - __fiq
+ - register
+ - extern
+ :c_calling_conventions:
+ - __stdcall
+ - __cdecl
+ - __fastcall
+ :treat_externs: :exclude
+ :treat_inlines: :exclude
+ :treat_as:
+ uint8: HEX8
+ uint16: HEX16
+ uint32: UINT32
+ int8: INT8
+ bool: UINT8
+ :memcmp_if_unknown: true
+ :when_ptr: :compare_data
+ :enforce_strict_ordering: true
+ :fail_on_unexpected_calls: true
+ :callback_include_count: true
+ :callback_after_arg_check: false
+ :exclude_setjmp_h: false
+
+:unity:
+ :use_param_tests: true
+ :defines:
+ - UNITY_EXCLUDE_FLOAT
+
+:environment: []
+
+:libraries:
+ :placement: :end
+ :flag: "-l${1}"
+ :path_flag: "-L ${1}"
+ :system: []
+ :test: []
+ :release: []
diff --git a/stubs/functions.stub.php b/stubs/functions.stub.php
new file mode 100644
index 0000000..120c867
--- /dev/null
+++ b/stubs/functions.stub.php
@@ -0,0 +1,6 @@
+
+#include
+
+/* Module startup */
+PHP_MINIT_FUNCTION(template_ext)
+{
+ return SUCCESS;
+}
+
+/* Module shutdown */
+PHP_MSHUTDOWN_FUNCTION(template_ext)
+{
+ return SUCCESS;
+}
+
+/* Request startup */
+PHP_RINIT_FUNCTION(template_ext)
+{
+ return SUCCESS;
+}
+
+/* Request shutdown */
+PHP_RSHUTDOWN_FUNCTION(template_ext)
+{
+ return SUCCESS;
+}
+
+/* Module info */
+PHP_MINFO_FUNCTION(template_ext)
+{
+ php_info_print_table_start();
+ php_info_print_table_header(2, "template_ext", "enabled");
+ php_info_print_table_end();
+
+ DISPLAY_INI_ENTRIES();
+}
+
+/* Helper function to check if a character is uppercase */
+static inline int is_uppercase(char c)
+{
+ return c >= 'A' && c <= 'Z';
+}
+
+/* Helper function to check if a character is alphanumeric */
+static inline int is_alphanumeric(char c)
+{
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
+}
+
+/* Converts a string to snake_case */
+PHP_FUNCTION(to_snake)
+{
+ char *input;
+ size_t input_len;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STRING(input, input_len)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (input_len == 0) {
+ RETURN_EMPTY_STRING();
+ }
+
+ // Allocate output buffer (worst case: every char becomes underscore + char)
+ char *output = emalloc(input_len * 2 + 1);
+ size_t output_len = 0;
+ int prev_was_separator = 0;
+
+ for (size_t i = 0; i < input_len; i++) {
+ char c = input[i];
+
+ // Handle separators: convert -, space to underscore
+ if (c == '-' || c == ' ') {
+ if (output_len > 0 && !prev_was_separator) {
+ output[output_len++] = '_';
+ }
+ prev_was_separator = 1;
+ continue;
+ }
+
+ // Handle uppercase letters
+ if (is_uppercase(c)) {
+ // Add underscore before uppercase letter (except at start)
+ if (output_len > 0 && !prev_was_separator) {
+ // Check if previous character was lowercase (camelCase)
+ if (i > 0 && islower(input[i-1])) {
+ output[output_len++] = '_';
+ }
+ }
+ output[output_len++] = tolower(c);
+ prev_was_separator = 0;
+ } else if (is_alphanumeric(c)) {
+ // Just copy lowercase letters and numbers
+ output[output_len++] = c;
+ prev_was_separator = 0;
+ } else {
+ // Skip other special characters
+ if (output_len > 0 && !prev_was_separator) {
+ prev_was_separator = 1;
+ }
+ }
+ }
+
+ // Remove trailing underscore
+ while (output_len > 0 && output[output_len - 1] == '_') {
+ output_len--;
+ }
+
+ output[output_len] = '\0';
+
+ RETVAL_STRINGL(output, output_len);
+ efree(output);
+}
+
+/* Module entry */
+zend_module_entry template_ext_module_entry = {
+ STANDARD_MODULE_HEADER,
+ EXTENSION_NAME,
+ ext_functions,
+ PHP_MINIT(template_ext),
+ PHP_MSHUTDOWN(template_ext),
+ PHP_RINIT(template_ext),
+ PHP_RSHUTDOWN(template_ext),
+ PHP_MINFO(template_ext),
+ EXTENSION_VERSION,
+ STANDARD_MODULE_PROPERTIES
+};
+
+#ifdef COMPILE_DL_TEMPLATE_EXT
+ZEND_GET_MODULE(template_ext)
+#endif
diff --git a/template_ext.h b/template_ext.h
new file mode 100644
index 0000000..8abb62a
--- /dev/null
+++ b/template_ext.h
@@ -0,0 +1,16 @@
+/*
+ * Header for template-ext PHP extension
+ */
+
+#ifndef TEMPLATE_EXT_H
+#define TEMPLATE_EXT_H
+
+#define EXTENSION_NAME "template_ext"
+#define EXTENSION_VERSION "1.0.0"
+
+static const zend_function_entry ext_functions[] = {
+ ZEND_FE(to_snake, NULL)
+ ZEND_FE_END
+};
+
+#endif /* TEMPLATE_EXT_H */
diff --git a/tests/c/support/test_helpers.c b/tests/c/support/test_helpers.c
new file mode 100644
index 0000000..ed80506
--- /dev/null
+++ b/tests/c/support/test_helpers.c
@@ -0,0 +1,70 @@
+#include "unity.h"
+#include "test_helpers.h"
+#include
+#include
+#include
+
+// Helper function to check if a character is uppercase
+static inline int is_uppercase(char c)
+{
+ return c >= 'A' && c <= 'Z';
+}
+
+// String to snake_case conversion function
+char* to_snake_calc(const char* input)
+{
+ if (input == NULL || strlen(input) == 0) {
+ char* result = emalloc(1);
+ result[0] = '\0';
+ return result;
+ }
+
+ // Allocate output buffer (worst case: every char becomes underscore + char)
+ char* output = emalloc(strlen(input) * 2 + 1);
+ size_t output_len = 0;
+ int prev_was_separator = 0;
+
+ for (size_t i = 0; input[i]; i++) {
+ char c = input[i];
+
+ // Handle separators: convert -, space to underscore
+ if (c == '-' || c == ' ') {
+ if (output_len > 0 && !prev_was_separator) {
+ output[output_len++] = '_';
+ }
+ prev_was_separator = 1;
+ continue;
+ }
+
+ // Handle uppercase letters
+ if (is_uppercase(c)) {
+ // Add underscore before uppercase letter (except at start)
+ if (output_len > 0 && !prev_was_separator) {
+ // Check if previous character was lowercase (camelCase)
+ if (i > 0 && islower(input[i-1])) {
+ output[output_len++] = '_';
+ }
+ }
+ output[output_len++] = tolower(c);
+ prev_was_separator = 0;
+ } else if (isalnum(c)) {
+ // Just copy lowercase letters and numbers
+ output[output_len++] = c;
+ prev_was_separator = 0;
+ } else {
+ // Skip other special characters
+ if (output_len > 0 && !prev_was_separator) {
+ prev_was_separator = 1;
+ }
+ }
+ }
+
+ // Remove trailing underscore
+ while (output_len > 0 && output[output_len - 1] == '_') {
+ output_len--;
+ }
+
+ output[output_len] = '\0';
+
+ return output;
+}
diff --git a/tests/c/support/test_helpers.h b/tests/c/support/test_helpers.h
new file mode 100644
index 0000000..c16061e
--- /dev/null
+++ b/tests/c/support/test_helpers.h
@@ -0,0 +1,7 @@
+#ifndef TEST_HELPERS_H
+#define TEST_HELPERS_H
+
+// String to snake_case conversion function for testing
+char* to_snake_calc(const char* input);
+
+#endif /* TEST_HELPERS_H */
diff --git a/tests/c/test_to_snake.c b/tests/c/test_to_snake.c
new file mode 100644
index 0000000..572f10a
--- /dev/null
+++ b/tests/c/test_to_snake.c
@@ -0,0 +1,155 @@
+#include "unity.h"
+#include "test_helpers.h"
+
+// ============= Basic Cases =============
+
+TEST_CASE("FirstName", "first_name")
+TEST_CASE("firstName", "first_name")
+TEST_CASE("first_name", "first_name")
+TEST_CASE("first-name", "first_name")
+TEST_CASE("first name", "first_name")
+TEST_CASE("Name", "name")
+TEST_CASE("HTTPResponse", "http_response")
+TEST_CASE("user2Name", "user2_name")
+TEST_CASE("getHTTPResponseCode", "get_http_response_code")
+TEST_CASE("IOError", "io_error")
+void test_to_snake_case_basic(const char* input, const char* expected) {
+ char* result = to_snake_calc(input);
+ TEST_ASSERT_EQUAL_STRING(expected, result);
+ efree(result);
+}
+
+// ============= Edge Cases =============
+
+void test_to_snake_empty_string(void) {
+ char* result = to_snake_calc("");
+ TEST_ASSERT_EQUAL_STRING("", result);
+ efree(result);
+}
+
+void test_to_snake_single_character(void) {
+ char* result = to_snake_calc("a");
+ TEST_ASSERT_EQUAL_STRING("a", result);
+ efree(result);
+
+ result = to_snake_calc("A");
+ TEST_ASSERT_EQUAL_STRING("a", result);
+ efree(result);
+}
+
+void test_to_snake_all_uppercase(void) {
+ char* result = to_snake_calc("HELLO");
+ TEST_ASSERT_EQUAL_STRING("hello", result);
+ efree(result);
+}
+
+void test_to_snake_all_lowercase(void) {
+ char* result = to_snake_calc("hello");
+ TEST_ASSERT_EQUAL_STRING("hello", result);
+ efree(result);
+}
+
+void test_to_snake_numbers(void) {
+ char* result = to_snake_calc("user2Name");
+ TEST_ASSERT_EQUAL_STRING("user2_name", result);
+ efree(result);
+
+ result = to_snake_calc("123ABC");
+ TEST_ASSERT_EQUAL_STRING("123abc", result);
+ efree(result);
+}
+
+void test_to_snake_multiple_separators(void) {
+ char* result = to_snake_calc("first name");
+ TEST_ASSERT_EQUAL_STRING("first_name", result);
+ efree(result);
+
+ result = to_snake_calc("first--name");
+ TEST_ASSERT_EQUAL_STRING("first_name", result);
+ efree(result);
+
+ result = to_snake_calc("first - name");
+ TEST_ASSERT_EQUAL_STRING("first_name", result);
+ efree(result);
+}
+
+void test_to_snake_leading_trailing_separators(void) {
+ char* result = to_snake_calc(" first_name ");
+ TEST_ASSERT_EQUAL_STRING("first_name", result);
+ efree(result);
+
+ result = to_snake_calc("-first_name-");
+ TEST_ASSERT_EQUAL_STRING("first_name", result);
+ efree(result);
+}
+
+// ============= Special Characters =============
+
+void test_to_snake_special_chars(void) {
+ // Special characters should be removed
+ char* result = to_snake_calc("first@name");
+ TEST_ASSERT_EQUAL_STRING("first_name", result);
+ efree(result);
+
+ result = to_snake_calc("first.name");
+ TEST_ASSERT_EQUAL_STRING("firstname", result);
+ efree(result);
+}
+
+// ============= Consecutive Uppercase =============
+
+void test_to_snake_consecutive_uppercase(void) {
+ char* result = to_snake_calc("HTTPResponse");
+ TEST_ASSERT_EQUAL_STRING("http_response", result);
+ efree(result);
+
+ result = to_snake_calc("XMLParser");
+ TEST_ASSERT_EQUAL_STRING("xml_parser", result);
+ efree(result);
+
+ result = to_snake_calc("HTMLElement");
+ TEST_ASSERT_EQUAL_STRING("html_element", result);
+ efree(result);
+}
+
+// ============= Test Matrix for Multiple Values =============
+
+// Test that single words remain unchanged
+TEST_MATRIX(["hello", "world", "test", "name"])
+void test_to_snake_single_word(const char* input) {
+ char* result = to_snake_calc(input);
+ TEST_ASSERT_EQUAL_STRING(input, result);
+ efree(result);
+}
+
+// Test camelCase to snake_case
+TEST_MATRIX(["camelCase", "PascalCase", "longVariableName"])
+void test_to_snake_camel_case(const char* input) {
+ char* result = to_snake_calc(input);
+ // camelCase -> camel_case, PascalCase -> pascal_case, longVariableName -> long_variable_name
+ char* expected = emalloc(strlen(input) * 2 + 1);
+ size_t j = 0;
+ for (size_t i = 0; input[i]; i++) {
+ if (isupper(input[i]) && i > 0 && islower(input[i-1])) {
+ expected[j++] = '_';
+ }
+ expected[j++] = tolower(input[i]);
+ }
+ expected[j] = '\0';
+ TEST_ASSERT_EQUAL_STRING(expected, result);
+ efree(expected);
+ efree(result);
+}
+
+// Test that all uppercase single words become lowercase
+TEST_MATRIX(["HELLO", "WORLD", "TEST", "NAME"])
+void test_to_snake_all_uppercase_single_word(const char* input) {
+ char* result = to_snake_calc(input);
+ char expected[100];
+ for (size_t i = 0; input[i]; i++) {
+ expected[i] = tolower(input[i]);
+ }
+ expected[strlen(input)] = '\0';
+ TEST_ASSERT_EQUAL_STRING(expected, result);
+ efree(result);
+}
diff --git a/tests/php/Integration/ToSnakeTest.php b/tests/php/Integration/ToSnakeTest.php
new file mode 100644
index 0000000..14f3a1e
--- /dev/null
+++ b/tests/php/Integration/ToSnakeTest.php
@@ -0,0 +1,72 @@
+toBe('camel_case');
+ expect(to_snake('PascalCase'))->toBe('pascal_case');
+ expect(to_snake('longVariableName'))->toBe('long_variable_name');
+});
+
+it('converts PascalCase to snake_case', function () {
+ expect(to_snake('FirstName'))->toBe('first_name');
+ expect(to_snake('HTTPResponse'))->toBe('http_response');
+ expect(to_snake('XMLParser'))->toBe('xml_parser');
+});
+
+it('converts kebab-case to snake_case', function () {
+ expect(to_snake('first-name'))->toBe('first_name');
+ expect(to_snake('long-variable-name'))->toBe('long_variable_name');
+});
+
+it('converts space separated to snake_case', function () {
+ expect(to_snake('first name'))->toBe('first_name');
+ expect(to_snake('long variable name'))->toBe('long_variable_name');
+});
+
+it('handles already snake_case strings', function () {
+ expect(to_snake('first_name'))->toBe('first_name');
+ expect(to_snake('long_variable_name'))->toBe('long_variable_name');
+});
+
+it('handles empty string', function () {
+ expect(to_snake(''))->toBe('');
+});
+
+it('handles single character', function () {
+ expect(to_snake('a'))->toBe('a');
+ expect(to_snake('A'))->toBe('a');
+});
+
+it('handles all uppercase', function () {
+ expect(to_snake('HELLO'))->toBe('hello');
+ expect(to_snake('WORLD'))->toBe('world');
+});
+
+it('handles all lowercase', function () {
+ expect(to_snake('hello'))->toBe('hello');
+ expect(to_snake('world'))->toBe('world');
+});
+
+it('handles numbers', function () {
+ expect(to_snake('user2Name'))->toBe('user2_name');
+ expect(to_snake('123ABC'))->toBe('123abc');
+});
+
+it('handles multiple separators', function () {
+ expect(to_snake('first name'))->toBe('first_name');
+ expect(to_snake('first--name'))->toBe('first_name');
+});
+
+it('handles leading and trailing separators', function () {
+ expect(to_snake(' first_name '))->toBe('first_name');
+ expect(to_snake('-first_name-'))->toBe('first_name');
+});
+
+it('handles consecutive uppercase letters', function () {
+ expect(to_snake('HTTPResponse'))->toBe('http_response');
+ expect(to_snake('XMLParser'))->toBe('xml_parser');
+ expect(to_snake('IOError'))->toBe('io_error');
+});
diff --git a/tests/php/Pest.php b/tests/php/Pest.php
new file mode 100644
index 0000000..d4db083
--- /dev/null
+++ b/tests/php/Pest.php
@@ -0,0 +1,8 @@
+group('integration')->in('Integration');