diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba47e82..c3103eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,12 +6,12 @@ on: workflow_dispatch: jobs: - build: + build-and-test: runs-on: ubuntu-latest strategy: matrix: - php-version: ['8.0', '8.1', '8.2', '8.3'] + php-version: ['8.1', '8.2', '8.3'] steps: - name: Checkout code @@ -23,8 +23,10 @@ jobs: php-version: ${{ matrix.php-version }} extensions: mbstring, intl - - name: Install build tools - run: sudo apt-get update && sudo apt-get install -y autoconf automake libtool bison re2c + - name: Install build tools and Valgrind + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool bison re2c valgrind - name: Prepare build run: | @@ -33,9 +35,69 @@ jobs: make - name: Run tests + id: run_tests run: make test + continue-on-error: true + + - name: Run demo with debug logging + id: run_demo + run: | + php -dextension=./modules/rayaop.so -i | grep rayaop + timeout 60s php -n -dextension=./modules/rayaop.so -dmemory_limit=128M -dreport_memleaks=1 -dzend.assertions=1 -dassert.exception=1 smoke.php + continue-on-error: true + + - name: Run Valgrind memory check + if: steps.run_tests.outcome == 'failure' || steps.run_demo.outcome == 'failure' + run: | + cat << EOF > valgrind.supp + { + + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:php_module_startup + ... + } + EOF + valgrind --suppressions=valgrind.supp --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --log-file=valgrind-out.txt php -n -dextension=./modules/rayaop.so smoke.php + + - name: Check Valgrind results + if: steps.run_tests.outcome == 'failure' || steps.run_demo.outcome == 'failure' + run: | + if [ -f valgrind-out.txt ]; then + echo "Valgrind log found:" + cat valgrind-out.txt + if ! grep -q "ERROR SUMMARY: 0 errors from 0 contexts" valgrind-out.txt; then + echo "Valgrind found errors" + exit 1 + fi + else + echo "Valgrind log not found. This is unexpected." + exit 1 + fi + + - name: Upload Valgrind log file + if: (steps.run_tests.outcome == 'failure' || steps.run_demo.outcome == 'failure') && always() + uses: actions/upload-artifact@v2 + with: + name: valgrind-log + path: valgrind-out.txt + if-no-files-found: warn + + - name: Upload test logs + if: failure() + uses: actions/upload-artifact@v2 + with: + name: test-logs + path: | + tests/*.log + tests/*.sh + if-no-files-found: warn - - name: Run demo + - name: Final status check + if: always() run: | - php -d extension=./modules/rayaop.so -i | grep rayaop - timeout 60s php -d extension=./modules/rayaop.so -d memory_limit=128M -d report_memleaks=1 -d zend.assertions=1 -d assert.exception=1 rayaop.php 2> php_stderr.log || true + if [ "${{ steps.run_tests.outcome }}" == "failure" ] || [ "${{ steps.run_demo.outcome }}" == "failure" ]; then + echo "Tests or demo run failed. Please check the logs for more information." + exit 1 + fi diff --git a/.gitignore b/.gitignore index 96cf1bc..c465303 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ # Ignore directories and its content /Makefile.fragments -/Makefile.objects \ No newline at end of file +/Makefile.objects +/cmake-build-debug \ No newline at end of file diff --git a/.idea/runConfigurations/clean.xml b/.idea/runConfigurations/clean.xml new file mode 100644 index 0000000..c0f66af --- /dev/null +++ b/.idea/runConfigurations/clean.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/make.xml b/.idea/runConfigurations/make.xml new file mode 100644 index 0000000..a7a7de3 --- /dev/null +++ b/.idea/runConfigurations/make.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..05b0e53 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,57 @@ +# 必要なCMakeのバージョンを指定 +# link https://cmake.org/cmake/help/latest/command/cmake_minimum_required.html +cmake_minimum_required(VERSION 3.8) + +# プロジェクトの名前と使用する言語を指定 +# link https://cmake.org/cmake/help/latest/command/project.html +project(rayaop C) + +# コンパイル時に定義するシンボルを指定 +# link https://cmake.org/cmake/help/latest/command/add_compile_definitions.html +add_compile_definitions(HAVE_RAYAOP) + + +# `php-config` コマンドを使ってPHPのインクルードディレクトリを取得 +# link https://cmake.org/cmake/help/latest/command/execute_process.html +execute_process ( + COMMAND php-config --include-dir + OUTPUT_VARIABLE PHP_SOURCE +) + +# 取得したディレクトリの末尾にある改行を削除 +# link https://cmake.org/cmake/help/latest/command/string.html +string(REGEX REPLACE "\n$" "" PHP_SOURCE "${PHP_SOURCE}") + +# 使用するソースディレクトリをメッセージとして表示 +# link https://cmake.org/cmake/help/latest/command/message.html +message("Using source directory: ${PHP_SOURCE}") + +# インクルードディレクトリを追加 +# link https://cmake.org/cmake/help/latest/command/include_directories.html +include_directories(${PHP_SOURCE}) +include_directories(${PHP_SOURCE}/main) +include_directories(${PHP_SOURCE}/Zend) +include_directories(${PHP_SOURCE}/TSRM) +include_directories(${PROJECT_SOURCE_DIR}) + +# カスタムターゲット `configure` を追加 +# `phpize` と `./configure` を実行し、ソースファイルに依存させる +# link https://cmake.org/cmake/help/latest/command/add_custom_target.html +add_custom_target(configure + COMMAND phpize && ./configure + DEPENDS ${SOURCE_FILES} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) + +# ソースファイルのリストを指定 +# link https://cmake.org/cmake/help/latest/command/set.html +set(SOURCE_FILES php_rayaop.h rayaop.c) + +# ソースファイルからライブラリを作成(ただし、ALLビルドからは除外) +# link https://cmake.org/cmake/help/latest/command/add_library.html +add_library(___ EXCLUDE_FROM_ALL ${SOURCE_FILES}) + +# CMakeの CMP0115 ポリシーの振る舞いを設定。新しいポリシーでは、ソースファイルが存在しない場合でも +# add_executable() または add_library() を実行しますが、生成されたビルドがそのファイルを見つけられない場合にはエラーを生成します。 +# link https://cmake.org/cmake/help/latest/policy/CMP0115.html +cmake_policy(SET CMP0115 NEW) + diff --git a/Makefile.frag b/Makefile.frag new file mode 100644 index 0000000..43f3386 --- /dev/null +++ b/Makefile.frag @@ -0,0 +1,16 @@ +clean-tests: + rm -f tests/*.diff tests/*.exp tests/*.log tests/*.out tests/*.php tests/*.sh + +mrproper: clean clean-tests + rm -rf autom4te.cache build modules vendor + rm -f acinclude.m4 aclocal.m4 config.guess config.h config.h.in config.log config.nice config.status config.sub \ + configure configure.ac install-sh libtool ltmain.sh Makefile Makefile.fragments Makefile.global \ + Makefile.objects missing mkinstalldirs run-tests.php *~ + +info: $(all_targets) + "$(PHP_EXECUTABLE)" -d "extension=$(phplibdir)/$(PHP_PECL_EXTENSION).so" --re "$(PHP_PECL_EXTENSION)" + +package.xml: php_$(PHP_PECL_EXTENSION).h + $(PHP_EXECUTABLE) build-packagexml.php + +.PHONY: all clean install distclean test prof-gen prof-clean prof-use clean-tests mrproper info \ No newline at end of file diff --git a/README.md b/README.md index 52d0815..6e00b84 100644 --- a/README.md +++ b/README.md @@ -4,36 +4,37 @@ ray-di logo -A PHP extension that provides Aspect-Oriented Programming (AOP) functionality for method interception. +A PHP extension that provides Aspect-Oriented Programming (AOP) functionality for method interception, designed to complement Ray.Aop. ## Features -- Intercept method calls on specific classes +- Efficient method interception for specific classes - Apply custom logic before and after method execution -- Modify method arguments and return values +- Works with `final` classes and methods ## Requirements -- PHP 8.0 or higher -- php-dev package installed +- PHP 8.1 or higher ## Installation -2.Compile the extension: +1. Clone the repository: ``` -phpize -./configure -make +git clone https://github.com/ray-di/ext-rayaop.git +cd ext-rayaop ``` -3. Install the extension: +2. Build and install the extension: ``` -sudo make install +phpize +./configure +make +make install ``` -4. Add the following line to your php.ini file: +3. Add the following line to your php.ini file: ``` extension=rayaop.so @@ -41,9 +42,12 @@ extension=rayaop.so ## About this Extension -This PECL extension is designed to enhance the performance of Ray.Aop by eliminating the need for CodeGen, resulting in faster execution speeds. While it is primarily created for Ray.Aop, it can also be used to implement custom AOP solutions independently of Ray.Aop. +This PECL extension is designed to enhance Ray.Aop by providing method interception capabilities at a lower level. While it can be used independently, it's primarily intended to be used in conjunction with Ray.Aop for optimal functionality. -By using this extension, developers can achieve high-performance method interception without the overhead of generating and compiling additional code. +Key points: +- The extension intentionally binds only one interceptor per method for simplicity and performance. +- Multiple interceptor chaining should be implemented in PHP code, either using Ray.Aop or custom implementation. +- The main advantages are the ability to intercept `final` classes/methods and unrestricted use of the `new` keyword. ## Usage @@ -63,14 +67,9 @@ class MyInterceptor implements Ray\Aop\MethodInterceptorInterface { public function intercept(object $object, string $method, array $params): mixed { - echo "Intercepted: " . get_class($object) . "::{$method}\n"; - echo "Arguments: " . json_encode($params) . "\n"; - - // Call the original method + echo "Before method execution\n"; $result = call_user_func_array([$object, $method], $params); - - echo "Method execution completed.\n"; - + echo "After method execution\n"; return $result; } } @@ -85,7 +84,7 @@ $interceptor = new MyInterceptor(); method_intercept('TestClass', 'testMethod', $interceptor); ``` -### Example +### Complete Example ```php class TestClass @@ -97,22 +96,47 @@ class TestClass } } +$interceptor = new MyInterceptor(); +method_intercept('TestClass', 'testMethod', $interceptor); + $test = new TestClass(); $result = $test->testMethod("test"); -echo "Result: $result\n"; +echo "Final result: $result\n"; ``` -## Running Tests - -To run the tests, use the following command: - +Output: ``` -php -dextension=./modules/rayaop.so test/rayaop_test.php +Before method execution +TestClass::testMethod(test) called +After method execution +Final result: Result: test ``` +## Integration with Ray.Aop + +For more complex AOP scenarios, it's recommended to use this extension in combination with [Ray.Aop](https://github.com/ray-di/Ray.Aop). Ray.Aop provides a higher-level API for managing multiple interceptors and more advanced AOP features. + +## The Power of AOP + +Aspect-Oriented Programming (AOP) is a powerful paradigm that complements Object-Oriented Programming (OOP) in building more flexible and maintainable software systems. By using AOP: + +1. **Separation of Concerns**: You can cleanly separate cross-cutting concerns (like logging, security, or transaction management) from your core business logic. + +2. **Enhanced Modularity**: AOP allows you to modularize system-wide concerns that would otherwise be scattered across multiple classes. + +3. **Improved Code Reusability**: Aspects can be reused across different parts of your application, reducing code duplication. + +4. **Easier Maintenance**: By centralizing certain behaviors, AOP can make your codebase easier to maintain and evolve over time. + +5. **Non-invasive Changes**: You can add new behaviors to existing code without modifying the original classes, adhering to the Open/Closed Principle. + +6. **Dynamic Behavior Modification**: With this PECL extension, you can even apply aspects to final classes and methods, providing unprecedented flexibility in your system design. + +By combining the strengths of OOP and AOP, developers can create more robust, flexible, and easier-to-maintain software architectures. This PECL extension, especially when used in conjunction with Ray.Aop, opens up new possibilities for structuring your PHP applications, allowing you to tackle complex problems with elegance and efficiency. + ## Build Script -You can use the build.sh script for various build operations: +The `build.sh` script provides various operations for building and managing the extension: ```sh ./build.sh clean # Clean the build environment @@ -122,10 +146,23 @@ You can use the build.sh script for various build operations: ./build.sh all # Execute all the above steps ``` -## Contributing +Use `./build.sh all` for a complete build and installation process. + +## Running Tests + +To run the tests for this extension, use the following command: + +```sh +make test +``` + +This command will execute the PHP extension's test suite, which is the standard method for testing PHP extensions. + +If you need to run specific tests or want more verbose output, you can use: -Contributions are welcome! Please feel free to submit a Pull Request. +```sh +make test TESTS="-v tests/your_specific_test.phpt" +``` -## License +Replace `your_specific_test.phpt` with the actual test file you want to run. -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/autoconf/pecl.m4 b/autoconf/pecl.m4 new file mode 100644 index 0000000..ffa45ac --- /dev/null +++ b/autoconf/pecl.m4 @@ -0,0 +1,415 @@ + +yes() { + true +} +no() { + false +} +dnl +dnl PECL_INIT(name) +dnl +dnl Start configuring the PECL extension. +dnl +AC_DEFUN([PECL_INIT], [dnl + m4_define([PECL_NAME],[$1])dnl +])dnl +dnl +dnl +dnl PECL_VAR(name) +dnl +AC_DEFUN([PECL_VAR], [dnl +AS_TR_CPP([PHP_]PECL_NAME[_$1])dnl +])dnl +dnl +dnl PECL_CACHE_VAR(name) +dnl +AC_DEFUN([PECL_CACHE_VAR], [dnl +AS_TR_SH([PECL_cv_$1])dnl +])dnl +dnl +dnl PECL_SAVE_VAR(name) +dnl +AC_DEFUN([PECL_SAVE_VAR], [dnl +AS_TR_SH([PECL_sv_$1])dnl +])dnl +dnl +dnl PECL_DEFINE(what, to[, desc]) +dnl +AC_DEFUN([PECL_DEFINE], [dnl + AC_DEFINE(PECL_VAR([$1]), ifelse([$2],,1,[$2]), ifelse([$3],,[ ],[$3])) +])dnl +dnl +dnl PECL_DEFINE_UQ(what, to[, desc]) +dnl +AC_DEFUN([PECL_DEFINE_UQ], [dnl + AC_DEFINE_UNQUOTED(PECL_VAR([$1]), [$2], ifelse([$3],,[ ],[$3])) +])dnl +dnl +dnl PECL_DEFINE_SH(what, to[, desc]) +dnl +AC_DEFUN([PECL_DEFINE_SH], [dnl + PECL_VAR([$1])=$2 + PECL_DEFINE_UQ([$1], [$2], [$3]) +]) +dnl +dnl PECL_DEFINE_FN(fn) +dnl +AC_DEFUN([PECL_DEFINE_FN], [ + AC_DEFINE(AS_TR_CPP([HAVE_$1]), [1], [ ]) +]) +dnl +dnl PECL_SAVE_ENV(var, ns) +dnl +AC_DEFUN([PECL_SAVE_ENV], [ + PECL_SAVE_VAR([$2_$1])=[$]$1 +]) +dnl +dnl PECL_RESTORE_ENV(var, ns) +dnl +AC_DEFUN([PECL_RESTORE_ENV], [ + $1=$PECL_SAVE_VAR([$2_$1]) +]) +dnl +dnl PECL_EVAL_LIBLINE(libline) +dnl +AC_DEFUN([PECL_EVAL_LIBLINE], [ + PECL_SAVE_ENV(ext_shared, pecl) + ext_shared=no + PHP_EVAL_LIBLINE([$1], _pecl_eval_libline_dummy) + PECL_RESTORE_ENV(ext_shared, pecl) +]) +dnl +dnl PECL_PROG_EGREP +dnl +dnl Checks for an egrep. Defines $EGREP. +dnl +AC_DEFUN([PECL_PROG_EGREP], [ + ifdef([AC_PROG_EGREP], [ + AC_PROG_EGREP + ], [ + AC_CHECK_PROG(EGREP, egrep, egrep) + ]) +]) +dnl +dnl PECL_PROG_AWK +dnl +dnl Checks for an awk. Defines $AWK. +dnl +AC_DEFUN([PECL_PROG_AWK], [ + ifdef([AC_PROG_AWK], [ + AC_PROG_AWK + ], [ + AC_CHECK_PROG(AWK, awk, awk) + ]) +]) +dnl +dnl PECL_PROG_SED +dnl +dnl Checks for the sed program. Defines $SED. +dnl +AC_DEFUN([PECL_PROG_SED], [ + ifdef([AC_PROG_SED], [ + AC_PROG_SED + ], [ + ifdef([LT_AC_PROG_SED], [ + LT_AC_PROG_SED + ], [ + AC_CHECK_PROG(SED, sed, sed) + ]) + ]) +]) +dnl +dnl PECL_PROG_PKGCONFIG +dnl +dnl Checks for pkg-config program and defines $PKG_CONFIG (to false if not found). +dnl +AC_DEFUN([PECL_PROG_PKGCONFIG], [ + if test -z "$PKG_CONFIG"; then + AC_PATH_PROG([PKG_CONFIG], [pkg-config], [false]) + fi +]) +dnl +dnl PECL_HAVE_PHP_EXT(name[, code-if-yes[, code-if-not]]) +dnl +dnl Check whether ext/$name is enabled in $PHP_EXECUTABLE (PECL build) +dnl or if $PHP_ is defined to anything else than "no" (in-tree build). +dnl Defines shell var PECL_VAR(HAVE_EXT_) to true or false. +dnl +AC_DEFUN([PECL_HAVE_PHP_EXT], [ + AC_REQUIRE([PECL_PROG_EGREP])dnl + AC_CACHE_CHECK([whether ext/$1 is enabled], PECL_CACHE_VAR([HAVE_EXT_$1]), [ + PECL_CACHE_VAR([HAVE_EXT_$1])=no + if test -x "$PHP_EXECUTABLE"; then + if $PHP_EXECUTABLE -m | $EGREP -q ^$1\$; then + PECL_CACHE_VAR([HAVE_EXT_$1])=yes + fi + elif test -n "$AS_TR_CPP([PHP_$1])" && test "$AS_TR_CPP([PHP_$1])" != "no"; then + PECL_CACHE_VAR([HAVE_EXT_$1])=yes + fi + ]) + if $PECL_CACHE_VAR([HAVE_EXT_$1]); then + PECL_VAR([HAVE_EXT_$1])=true + PECL_DEFINE([HAVE_EXT_$1]) + $2 + else + PECL_VAR([HAVE_EXT_$1])=false + $3 + fi +]) +dnl +dnl PECL_HAVE_PHP_EXT_HEADER(ext[, header]) +dnl +dnl Check where to find a header for ext and add the found dir to $INCLUDES. +dnl If header is not specified php_.h is assumed. +dnl Defines shell var PHP__EXT__INCDIR to the found dir. +dnl Defines PHP__HAVE_
to the found path. +dnl +AC_DEFUN([PECL_HAVE_PHP_EXT_HEADER], [dnl + AC_REQUIRE([PECL_PROG_SED])dnl + m4_define([EXT_HEADER], ifelse([$2],,php_$1.h,[$2]))dnl + AC_CACHE_CHECK([for EXT_HEADER of ext/$1], PECL_CACHE_VAR([EXT_$1]_INCDIR), [ + for i in $(printf "%s" "$INCLUDES" | $SED -e's/-I//g') $abs_srcdir ../$1; do + if test -d $i; then + for j in $i/EXT_HEADER $i/ext/$1/EXT_HEADER; do + if test -f $j; then + PECL_CACHE_VAR([EXT_$1]_INCDIR)=$(dirname "$j") + break + fi + done + fi + done + ]) + PECL_VAR([EXT_$1]_INCDIR)=$PECL_CACHE_VAR([EXT_$1]_INCDIR) + PHP_ADD_INCLUDE([$PECL_VAR([EXT_$1]_INCDIR)]) + PECL_DEFINE_UQ([HAVE_]EXT_HEADER, "$PECL_VAR([EXT_$1]_INCDIR)/EXT_HEADER") +]) +dnl +dnl PECL_HAVE_CONST(header, const[, type=int[, code-if-yes[, code-if-mno]]]) +dnl +AC_DEFUN([PECL_HAVE_CONST], [dnl + AC_REQUIRE([PECL_PROG_EGREP])dnl + AC_CACHE_CHECK([for $2 in $1], PECL_CACHE_VAR([HAVE_$1_$2]), [ + AC_TRY_COMPILE([ + #include "$1" + ], [ + ]ifelse([$3],,int,[$3])[ _c = $2; + (void) _c; + ], [ + PECL_CACHE_VAR([HAVE_$1_$2])=yes + ], [ + PECL_CACHE_VAR([HAVE_$1_$2])=no + ]) + ]) + if $PECL_CACHE_VAR([HAVE_$1_$2]); then + PECL_DEFINE([HAVE_$2]) + $4 + else + ifelse([$5],,:,[$5]) + fi +]) +dnl +dnl _PECL_TR_VERSION(version) +dnl +AC_DEFUN([_PECL_TR_VERSION], [dnl +AC_REQUIRE([PECL_PROG_AWK])dnl +$(printf "%s" $1 | $AWK -F "[.]" '{print $[]1*1000000 + $[]2*10000 + $[]3*100 + $[]4}') +]) +dnl +dnl PECL_CHECKED_VERSION(name) +dnl +dnl Shell var name of an already checked version. +dnl +AC_DEFUN([PECL_CHECKED_VERSION], [PECL_VAR([$1][_VERSION])]) +dnl +dnl PECL_HAVE_VERSION(name, min-version[, code-if-yes[, code-if-not]]) +dnl +dnl Perform a min-version check while in an PECL_CHECK_* block. +dnl Expands AC_MSG_ERROR when code-if-not is empty and the version check fails. +dnl +AC_DEFUN([PECL_HAVE_VERSION], [ + aversion=_PECL_TR_VERSION([$PECL_CHECKED_VERSION([$1])]) + mversion=_PECL_TR_VERSION([$2]) + AC_MSG_CHECKING([whether $1 version $PECL_CHECKED_VERSION([$1]) >= $2]) + if test -z "$aversion" || test "$aversion" -lt "$mversion"; then + ifelse($4,,AC_MSG_ERROR([no]), [ + AC_MSG_RESULT([no]) + $4 + ]) + else + AC_MSG_RESULT([ok]) + $3 + fi +]) +dnl +dnl PECL_CHECK_CUSTOM(name, path, header, lib, version) +dnl +AC_DEFUN([PECL_CHECK_CUSTOM], [ + PECL_SAVE_ENV([CPPFLAGS], [$1]) + PECL_SAVE_ENV([LDFLAGS], [$1]) + PECL_SAVE_ENV([LIBS], [$1]) + + AC_MSG_CHECKING([for $1]) + AC_CACHE_VAL(PECL_CACHE_VAR([$1_prefix]), [ + for path in $2 /usr/local /usr /opt; do + if test "$path" = "" || test "$path" = "yes" || test "$path" = "no"; then + continue + elif test -f "$path/include/$3"; then + PECL_CACHE_VAR([$1_prefix])="$path" + break + fi + done + ]) + if test -n "$PECL_CACHE_VAR([$1_prefix])"; then + CPPFLAGS="-I$PECL_CACHE_VAR([$1_prefix])/include" + LDFLAGS="-L$PECL_CACHE_VAR([$1_prefix])/$PHP_LIBDIR" + LIBS="-l$4" + PECL_EVAL_LIBLINE([$LDFLAGS $LIBS]) + + AC_CACHE_VAL(PECL_CACHE_VAR([$1_version]), [ + pushd $PECL_CACHE_VAR([$1_prefix]) >/dev/null + PECL_CACHE_VAR([$1_version])=$5 + popd >/dev/null + ]) + PECL_CHECKED_VERSION([$1])=$PECL_CACHE_VAR([$1_version]) + + if test -n "$PECL_CHECKED_VERSION([$1])"; then + PECL_VAR([HAVE_$1])=true + PECL_DEFINE([HAVE_$1]) + PECL_DEFINE_UQ($1[_VERSION], "$PECL_CHECKED_VERSION([$1])") + else + PECL_VAR([HAVE_$1])=false + fi + else + PECL_VAR([HAVE_$1])=false + fi + AC_MSG_RESULT([${PECL_CHECKED_VERSION([$1]):-no}]) +]) +dnl +dnl PECL_CHECK_CONFIG(name, prog-config, version-flag, cppflags-flag, ldflags-flag, libs-flag) +dnl +AC_DEFUN([PECL_CHECK_CONFIG], [ + PECL_SAVE_ENV([CPPFLAGS], [$1]) + PECL_SAVE_ENV([LDFLAGS], [$1]) + PECL_SAVE_ENV([LIBS], [$1]) + + + AC_MSG_CHECKING([for $1]) + ifelse($2, [$PKG_CONFIG $1], [ + AC_CACHE_VAL(PECL_CACHE_VAR([$1_exists]), [ + if $($2 --exists); then + PECL_CACHE_VAR([$1_exists])=yes + else + PECL_CACHE_VAR([$1_exists])=no + fi + ]) + if $PECL_CACHE_VAR([$1_exists]); then + ]) + AC_CACHE_VAL(PECL_CACHE_VAR([$1_version]), [ + PECL_CACHE_VAR([$1_version])=$($2 $3) + ]) + PECL_CHECKED_VERSION([$1])=$PECL_CACHE_VAR([$1_version]) + AC_CACHE_VAL(PECL_CACHE_VAR([$1_cppflags]), [ + PECL_CACHE_VAR([$1_cppflags])=$($2 $4) + ]) + CPPFLAGS=$PECL_CACHE_VAR([$1_cppflags]) + AC_CACHE_VAL(PECL_CACHE_VAR([$1_ldflags]), [ + PECL_CACHE_VAR([$1_ldflags])=$($2 $5) + ]) + LDFLAGS=$PECL_CACHE_VAR([$1_ldflags]) + AC_CACHE_VAL(PECL_CACHE_VAR([$1_libs]), [ + PECL_CACHE_VAR([$1_libs])=$($2 $6) + ]) + LIBS=$PECL_CACHE_VAR([$1_libs]) + PECL_EVAL_LIBLINE([$LDFLAGS $LIBS]) + ifelse($2, [$PKG_CONFIG $1], [ + fi + ]) + + if test -n "$PECL_CHECKED_VERSION([$1])"; then + PECL_VAR([HAVE_$1])=true + PECL_DEFINE([HAVE_$1]) + PECL_DEFINE_UQ([$1_VERSION], "$PECL_CHECKED_VERSION([$1])") + else + PECL_VAR([HAVE_$1])=false + fi + + AC_MSG_RESULT([${PECL_CHECKED_VERSION([$1]):-no}]) +]) +dnl +dnl PECL_CHECK_PKGCONFIG(pkg[, additional-pkg-config-path]) +dnl +AC_DEFUN([PECL_CHECK_PKGCONFIG], [dnl + AC_REQUIRE([PECL_PROG_PKGCONFIG])dnl + ifelse($2,,, [ + PECL_SAVE_VAR(pkgconfig_path)="$PKG_CONFIG_PATH" + if test -d "$2"; then + export PKG_CONFIG_PATH="$2/lib/pkgconfig:$PKG_CONFIG_PATH" + fi + ]) + PECL_CHECK_CONFIG([$1], [$PKG_CONFIG $1], [--modversion], [--cflags-only-I], [--libs-only-L], [--libs-only-l]) + ifelse($2,,, [ + PKG_CONFIG_PATH="$PECL_SAVE_VAR(pkgconfig_path)" + ]) +]) +dnl +dnl PECL_CHECK_DONE(name, success[, incline, libline]) +dnl +AC_DEFUN([PECL_CHECK_DONE], [ + if $2; then + incline=$CPPFLAGS + libline="$LDFLAGS $LIBS" + PECL_DEFINE([HAVE_$1]) + else + incline=$3 + libline=$4 + fi + + PECL_RESTORE_ENV([CPPFLAGS], [$1]) + PECL_RESTORE_ENV([LDFLAGS], [$1]) + PECL_RESTORE_ENV([LIBS], [$1]) + + PHP_EVAL_INCLINE([$incline]) + PHP_EVAL_LIBLINE([$libline], AS_TR_CPP(PECL_NAME[_SHARED_LIBADD])) +]) + +dnl +dnl PECL_CHECK_CA([additional-ca-paths,[ additional-ca-bundles]]) +dnl +AC_DEFUN([PECL_CHECK_CA], [ + AC_CACHE_CHECK([for default CA path], PECL_CACHE_VAR([CAPATH]), [ + PECL_VAR([CAPATH])= + for ca_path in $1 \ + /etc/ssl/certs \ + /System/Library/OpenSSL + do + # check if it's actually a hashed directory + if test -d "$ca_path" && ls "$ca_path"/@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@.0 >/dev/null 2>&1; then + PECL_CACHE_VAR([CAPATH])=$ca_path + break + fi + done + ]) + if test -n "$PECL_CACHE_VAR([CAPATH])"; then + PECL_DEFINE_SH([CAPATH], "$PECL_CACHE_VAR([CAPATH])") + fi + + AC_CACHE_CHECK([for default CA info], PECL_CACHE_VAR([CAINFO]), [ + for ca_info in $2 \ + /etc/ssl/{cert,ca-bundle}.pem \ + /{etc,usr/share}/ssl/certs/ca-{bundle,ceritifcates}.crt \ + /etc/{pki/ca-trust,ca-certificates}/extracted/pem/tls-ca-bundle.pem \ + /etc/pki/tls/certs/ca-bundle{,.trust}.crt \ + /usr/local/etc/{,open}ssl/cert.pem \ + /usr/local/share/certs/ca-root-nss.crt \ + /{usr,usr/local,opt}/local/share/curl/curl-ca-bundle.crt + do + if test -f "$ca_info"; then + PECL_CACHE_VAR([CAINFO])=$ca_info + break + fi + done + ]) + if test -n "$PECL_CACHE_VAR([CAINFO])"; then + PECL_DEFINE_SH([CAINFO], "$PECL_CACHE_VAR([CAINFO])") + fi +]) diff --git a/autoconf/php-executable.m4 b/autoconf/php-executable.m4 new file mode 100644 index 0000000..29a3e1d --- /dev/null +++ b/autoconf/php-executable.m4 @@ -0,0 +1,6 @@ +dnl +dnl Generate run-php bash script +dnl +AC_CONFIG_COMMANDS_POST([ + ln -s "$PHP_EXECUTABLE" build/php +]) diff --git a/build-packagexml.php b/build-packagexml.php new file mode 100644 index 0000000..a83d467 --- /dev/null +++ b/build-packagexml.php @@ -0,0 +1,452 @@ + + * @license PHP License + * @license MIT + */ + +declare (strict_types = 1); + +set_exception_handler(function (Throwable $exception): void { + $msg = ($exception instanceof RuntimeException ? "" : "Unexpected error: ") . $exception->getMessage(); + + fwrite(STDERR, "\033[0;31m" /* red */ . $msg . "\033[0m" . "\n"); + exit(1); +}); + +if (!extension_loaded('dom') || !extension_loaded('simplexml') || !extension_loaded('spl')) { + throw new RuntimeException("Following extensions are required: DOM, SimpleXML and SPL"); +} + +// 1. Load package.xml and create release +$package = (function (): PackageXMLElement { + return file_exists('package.xml') + ? simplexml_load_file('package.xml', PackageXMLElement::class) + : PackageXMLElement::create(); +})(); + +$release = new Release(); + +// 2. Process php_{extname}.h +(function () use ($package, $release): void { + $extname = (string)$package->name ?: getenv('PHP_PECL_EXTENSION') ?: null; + + if ($extname !== null) { + $filename = "php_{$extname}.h"; + + if (!file_exists($filename)) { + throw new RuntimeException("{$filename} not found"); + } + } else { + $filename = glob('php_*.h')[0] ?? null; + + if ($filename === null) { + throw new RuntimeException("Couldn't find the main header file (php_*.h)"); + } + + $extname = preg_replace('/^php_(.+)\.h$/', '$1', $filename); + } + + if ((string)$package->name === '') { + $package->name = $extname; + } + + + $macroPrefix = strtoupper(pathinfo($filename, PATHINFO_FILENAME)); + $contents = file_get_contents($filename); + + preg_match_all("/^[ \t]*#define\s+{$macroPrefix}_(?\w+)[ \t]+\"(?.+)\"/m", $contents, $matches, + PREG_PATTERN_ORDER); + $macros = array_combine($matches['key'], $matches['value']); + + // Package name + if (isset($macros['EXTNAME'])) { + if ((string)$package->name !== '' && (string)$package->name !== $macros['EXTNAME']) { + throw new RuntimeException("Package name '{$package->name}' (package.xml) doesn't match " + . "{$macroPrefix}_EXTNAME '{$macros['EXTNAME']}' ($filename)"); + } + } + + // Release version + if (!isset($macros['VERSION'])) { + throw new RuntimeException("$filename does not contain {$macroPrefix}_VERSION macro"); + } + $release->version = $macros['VERSION']; + + if (strpos($release->version, 'dev') !== false) { + throw new RuntimeException("Development versions shouldn't be released ({$macros['VERSION']}). " + . "Please change {$macroPrefix}_VERSION in $filename."); + } + if ($release->existsIn($package->changelog)) { + throw new RuntimeException("Version {$macros['VERSION']} already released. " + . "Please change {$macroPrefix}_VERSION in $filename."); + } +})(); + +// 3. Copy info from package to release +(function () use ($package, $release): void { + $release->license = (string)$package->license; + $release->licenseUri = (string)$package->license['uri']; + + if (strpos($release->version, 'alpha') !== false) { + $release->stability = 'alpha'; + } else if (strpos($release->version, 'beta') !== false || strpos($release->version, 'RC') !== false) { + $release->stability = 'beta'; + } else if (substr($release->version, 0, 1) === '0') { + $release->stability = ((string)$package->stability->release) ?: 'alpha'; + } else { + $release->stability = 'stable'; + } + + if ($release->isMajor((string)$package->version->release)) { + $release->apiVersion = $release->version; + $release->apiStability = $release->stability; + } else { + $release->apiVersion = (string)$package->version->api; + $release->apiStability = (string)$package->stability->api; + } +})(); + +// 4. Get release notes from stdin +$release->notes = (function (string $name, string $version): string { + if (function_exists('posix_isatty') && posix_isatty(STDIN)) { + fwrite(STDOUT, "Enter the release notes for $name $version (end with Ctrl-D):\n"); + } + + $notes = ''; + + do { + $notes .= fread(STDIN, 1024); + } while (!feof(STDIN)); + + return trim($notes); +})((string)$package->name, $release->version); + +// 5. Add release to package +if (!isset($package->changelog)) { + $package->addChild('changelog'); +} +$release->update($package->changelog->prependChild('release')); +$release->update($package); + +// 6. Removes files that no longer exist from package.xml +(function () use ($package): void { + $fileElements = $package->xpath('p:contents//p:file'); + + foreach ($fileElements as $element) { + $path = join("/", array_map('strval', $element->xpath('ancestor-or-self::*[@name!="/"]/@name'))); + + if (!file_exists($path)) { + unset($element[0]); + } + }; +})(); + +// 7. Add new files to package.xml +(function () use ($package): void { + $ext = ['c', 'h', 'phpt']; + + $dir = new RecursiveDirectoryIterator('.', FilesystemIterator::SKIP_DOTS | FilesystemIterator::CURRENT_AS_PATHNAME); + $itDir = new RecursiveIteratorIterator($dir); + $itReg = new RegexIterator($itDir, '~^./(.+\.(?:' . join('|', $ext) . '))$~', RegexIterator::GET_MATCH); + $files = iterable_column($itReg, 1); + + $newFiles = new CallbackFilterIterator($files, function ($path) use ($package): bool { + $file = basename($path); + $dirs = dirname($path) !== '.' ? explode('/', dirname($path)) : []; + array_unshift($dirs, '/'); + + $xpath = 'p:dir[@name="' . join('"]/p:dir[@name="', $dirs) . '"]/p:file[@name="' . $file . '"]'; + return count($package->contents->xpath($xpath)) === 0; + }); + + if (is_dir('.git')) { + $newFiles = gitignore_filter($newFiles); + } + + foreach ($newFiles as $file) { + $dirs = dirname($file) !== '.' ? explode('/', dirname($file)) : []; + + $dirElement = array_reduce($dirs, function (SimpleXMLElement $parent, string $dir): SimpleXMLElement { + $cur = $parent->xpath('p:dir[@name="' . $dir . '"]')[0] ?? null; + + if ($cur === null) { + $cur = $parent->addChild('dir'); + $cur['name'] = $dir; + } + + return $cur; + }, $package->contents->dir[0]); + + $fileElement = $dirElement->addChild('file'); + $fileElement['name'] = basename($file); + $fileElement['role'] = pathinfo($file, PATHINFO_EXTENSION) === 'phpt' ? 'test' : 'src'; + } +})(); + +// 8. Remove empty dirs from package.xml +(function () use ($package): void { + $emptyDirs = $package->xpath('p:contents//p:dir[not(descendant::*[local-name()="file"])]'); + + foreach (array_reverse($emptyDirs) as $element) { // reverse so children are deleted first + unset($element[0]); + } +})(); + +// 9. Sort files +(function () use ($package): void { + $dirElements = $package->xpath('p:contents//p:dir'); + + $sorter = function(SimpleXMLElement $first, SimpleXMLElement $second): int { + return + (($first->getName() !== 'file') <=> ($second->getName() !== 'file')) ?: + strcasecmp((string)$first['name'], (string)$second['name']); + }; + + foreach ($dirElements as $element) { + $element->sort($sorter); + } +})(); + +// 10. Save package.xml +$package->asXML('package.xml'); + +// :) friendly message +echo "\033[0;32m", /* green */ "Updated package.xml for {$package->name} {$release->version}", "\033[0;0m", "\n"; + +// ================= classes and functions ==================== + +class Release +{ + public $version; + public $apiVersion; + public $stability; + public $apiStability; + + public $license; + public $licenseUri; + public $notes; + + public function isMajor(string $oldVersion): bool + { + if ($oldVersion === '') { + return true; + } + + [$major, $minor] = explode('.', $this->version); + [$oldMajor, $oldMinor] = explode('.', $oldVersion); + + return ($major !== $oldMajor) || ($major === '0' && $minor !== $oldMinor); + } + + public function existsIn(SimpleXMLElement $changelog): bool + { + if (!isset($changelog->release)) { + return false; + } + + foreach ($changelog->release as $release) { + if ((string)($release->version->release) === $this->version) { + return true; + } + } + + return false; + } + + public function update(SimpleXMLElement $element) + { + $noTime = isset($element->date) && !isset($element->time); + + $element->date = date('Y-m-d'); + if (!$noTime) { + $element->time = date('H:i:s'); + } + + $element->version->release = $this->version; + $element->version->api = $this->apiVersion; + $element->stability->release = $this->stability; + $element->stability->api = $this->apiStability; + + $element->license = $this->license; + $element->license['uri'] = $this->licenseUri; + + $indent = str_repeat(' ', count($element->xpath('ancestor::*')) + 1); + $element->notes = "\n" . $this->notes . "\n$indent"; + } +}; + +class PackageXMLElement extends SimpleXMLElement +{ + private const BLANK_PACKAGE = << + + + pecl.php.net + + + + + + + yes + + + + + + + + + + + MIT License + + + + + + + + + + + + + + + + + + 7.0.0 + + + 1.4.3 + + + + + + + +XML; + + public static function create(): self + { + return new static(self::BLANK_PACKAGE); + } + + public function prependChild($name, $value = '') + { + $dom = dom_import_simplexml($this); + + $new = $dom->insertBefore( + $dom->ownerDocument->createElement($name, $value), + $dom->firstChild + ); + + return simplexml_import_dom($new, __CLASS__); + } + + public function sort(callable $sorter): void + { + if ($this->count() === 0) { + return; + } + + $children = iterator_to_array($this->children(), false); + usort($children, $sorter); + + $dom = dom_import_simplexml($this); + + while ($dom->hasChildNodes()) { + $dom->removeChild($dom->firstChild); + } + + foreach ($children as $child) { + $dom->appendChild(dom_import_simplexml($child)); + } + } + + public function asXML($filename = null) + { + $dom = new DOMDocument("1.0"); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML(parent::asXML()); + + $xml = $dom->saveXML(); + + // Go from 2 space indentation to 1. + $xml = preg_replace('~^([ ]+)\1<(?!/notes>)~m', '\1<', $xml); + + if ($filename === null) { + return $xml; + } + + if (file_exists($filename)) { + rename($filename, $filename . '~'); + } + file_put_contents($filename, $xml); + } + + public function xpath($xpath) + { + $this->registerXPathNamespace('p', 'http://pear.php.net/dtd/package-2.0'); + + return parent::xpath($xpath); + } +} + +function iterable_column(iterable $iterable, $column): Generator +{ + foreach ($iterable as $key => $item) { + yield $key => $item[$column]; + } +} + +function gitignore_filter(iterable $files): Generator +{ + $spec = [ + ["pipe", "r"], + ["pipe", "w"], + ["pipe", "w"], + ]; + + $process = proc_open('git check-ignore --non-matching --verbose --stdin', $spec, $pipes); + stream_set_timeout($pipes[1], 1); + stream_set_blocking($pipes[2], false); + + foreach ($files as $file) { + fwrite($pipes[0], $file . "\n"); + $line = fgets($pipes[1], 1024); + + if ($line === false || substr($line, 0, 2) === '::') { + yield $file; + } + } + + $err = fread($pipes[2], 10000); + + fclose($pipes[0]); + fclose($pipes[1]); + fclose($pipes[2]); + + $ret = proc_close($process); + + if ($ret !== 0 && $err !== '') { + fwrite(STDERR, "\033[0;31m" /* red */ . $err . "\033[0m" /* no color */ . "\n"); + throw new RuntimeException("Checking .gitignore failed"); + } +} diff --git a/build.sh b/build.sh index de64399..4117f68 100755 --- a/build.sh +++ b/build.sh @@ -24,7 +24,7 @@ install() { run() { echo "Run..." - php -dextension=modules/rayaop.so -ddisplay_errors=1 rayaop.php + php -dextension=modules/rayaop.so -ddisplay_errors=1 smoke.php } case $1 in diff --git a/config.m4 b/config.m4 index f6e73f3..9a27757 100644 --- a/config.m4 +++ b/config.m4 @@ -1,6 +1,37 @@ -# 'rayaop'サポートを有効にするかどうかの設定を追加 -PHP_ARG_ENABLE(rayaop, whether to enable rayaop support, -[ --enable-rayaop Enable rayaop support]) +dnl $Id$ +dnl config.m4 for extension rayaop -PHP_NEW_EXTENSION(rayaop, src/rayaop.c, $ext_shared) +dnl PECLの設定マクロをインクルード +dnl link https://github.com/php/pecl-tools/blob/master/autoconf/pecl.m4 +sinclude(./autoconf/pecl.m4) +dnl PHP実行ファイルの検出マクロをインクルード +dnl link https://github.com/php/pecl-tools/blob/master/autoconf/php-executable.m4 +sinclude(./autoconf/php-executable.m4) + +dnl PECL拡張の初期化 +dnl link https://github.com/php/pecl-tools/blob/master/pecl.m4#L229 +PECL_INIT([rayaop]) + +dnl 拡張機能を有効にするかどうかの設定オプションを追加 +dnl link https://www.gnu.org/software/autoconf/manual/autoconf-2.68/html_node/External-Shell-Variables.html +PHP_ARG_ENABLE(rayaop, whether to enable rayaop, [ --enable-rayaop Enable rayaop]) + +dnl 拡張機能が有効な場合の処理 +if test "$PHP_RAYAOP" != "no"; then + dnl 拡張機能が有効かどうかを定義 + dnl link https://www.gnu.org/software/autoconf/manual/autoconf-2.68/html_node/Defining-Variables.html + AC_DEFINE(HAVE_RAYAOP, 1, [whether rayaop is enabled]) + + dnl PHPの新しい拡張機能を追加 + dnl link https://www.phpinternalsbook.com/build_system/build_system.html + PHP_NEW_EXTENSION(rayaop, rayaop.c, $ext_shared) + + dnl Makefileフラグメントを追加 + dnl link https://www.phpinternalsbook.com/build_system/build_system.html#php-add-makefile-fragment + PHP_ADD_MAKEFILE_FRAGMENT + + dnl ヘッダーファイルのインストール指示を追加 + dnl link https://www.phpinternalsbook.com/build_system/build_system.html#php-install-headers + PHP_INSTALL_HEADERS([ext/rayaop], [php_rayaop.h]) +fi diff --git a/config.w32 b/config.w32 new file mode 100644 index 0000000..584e91d --- /dev/null +++ b/config.w32 @@ -0,0 +1,10 @@ +// $Id$ +// vim:ft=javascript + +ARG_ENABLE("rayaop", "enable rayaop", "no"); + +if (PHP_RAYAOP != "no") { + EXTENSION("rayaop", "rayaop.c"); + AC_DEFINE('HAVE_RAYAOP', 1 , 'whether rayaop is enabled'); + PHP_INSTALL_HEADERS("ext/rayaop/", "php_rayaop.h"); +} diff --git a/docs/policy.md b/docs/policy.md index 7ec2690..734b485 100644 --- a/docs/policy.md +++ b/docs/policy.md @@ -1,50 +1,50 @@ -# Ray.Aop PECL拡張 実装方針 +# Ray.Aop PECL Extension Implementation Policy -## 1. インターセプション機能の実装 +## 1. Implementation of Interception Functionality -- Zend Engineのフックを利用して、指定されたクラスとメソッドの呼び出しをインターセプトする機能を実装します。 -- オリジナルのメソッド呼び出しをラップし、インターセプトハンドラーを呼び出すメカニズムを構築します。 +- Utilize Zend Engine hooks to implement functionality that intercepts calls to specified classes and methods. +- Construct a mechanism that wraps original method calls and invokes intercept handlers. -## 2. MethodInterceptorInterfaceの実装 +## 2. Implementation of MethodInterceptorInterface -- PHPユーザーランドで定義されるMethodInterceptorInterfaceをC言語レベルで処理できるようにします。 -- interceptメソッドの呼び出しを適切に処理し、ユーザー定義のインターセプトロジックを実行できるようにします。 +- Enable C-level processing of the MethodInterceptorInterface defined in PHP userland. +- Properly handle the invocation of the intercept method, allowing execution of user-defined interception logic. -## 3. method_intercept関数の実装 +## 3. Implementation of method_intercept Function -- クラス名、メソッド名、MethodInterceptorInterfaceオブジェクトを受け取り、インターセプション情報を内部で管理する仕組みを作ります。 -- 登録されたインターセプション情報を効率的に保存し、高速にアクセスできるデータ構造を設計します。 +- Create a mechanism to internally manage interception information by receiving class name, method name, and MethodInterceptorInterface object. +- Design an efficient data structure to store and quickly access registered interception information. -## 4. パフォーマンスの最適化 +## 4. Performance Optimization -- インターセプション処理によるオーバーヘッドを最小限に抑えるため、効率的なコード生成やキャッシュ機構を検討します。 -- 不要な場合にはインターセプション処理をスキップするような最適化を行います。 +- Consider efficient code generation and caching mechanisms to minimize overhead caused by interception processing. +- Implement optimizations to skip interception processing when unnecessary. -## 5. PHP8互換性 +## 5. PHP 8 Compatibility -- 当初はPHP8のみをサポートし、PHP7との互換性は考慮しません。 -- 将来的なPHP7サポートの可能性を考慮し、拡張性のある設計を心がけます。 +- Initially support only PHP 8, without considering compatibility with PHP 7. +- Keep in mind extensible design for potential future PHP 7 support. -## 6. メモリ管理 +## 6. Memory Management -- インターセプション情報やオブジェクトの参照カウントを適切に管理し、メモリリークを防ぎます。 +- Properly manage reference counts of interception information and objects to prevent memory leaks. -## 7. エラーハンドリング +## 7. Error Handling -- 無効な引数や実行時エラーに対して適切に対応し、PHPのエラーハンドリング機構と統合します。 +- Appropriately handle invalid arguments and runtime errors, integrating with PHP's error handling mechanism. -## 8. 拡張性 +## 8. Extensibility -- 将来的な機能拡張や変更に対応できるよう、モジュール化された設計を心がけます。 +- Aim for a modularized design to accommodate future feature extensions and changes. -## 9. ユーザーランドでのマッチング +## 9. Userland Matching -- マッチング処理はユーザーランドで行われることを前提とします。 -- PECL拡張はインターセプターを直接呼び出すのではなく、インターセプトハンドラーを呼び出します。 +- Assume that matching processes will be performed in userland. +- The PECL extension will call intercept handlers rather than directly calling interceptors. -## 10. インターセプトハンドラーの呼び出し +## 10. Invocation of Intercept Handlers -- インターセプションが発生した際、登録されたインターセプトハンドラーを呼び出します。 -- インターセプトハンドラーがインターセプターの呼び出しを制御し、Ray.Aop以外のインターセプターとの連携を可能にします。 +- Call registered intercept handlers when interception occurs. +- Allow intercept handlers to control the invocation of interceptors, enabling integration with interceptors other than Ray.Aop. -これらの方針に基づいて実装を進めることで、効率的で信頼性の高いRay.Aop PECL拡張を開発します。 \ No newline at end of file +By proceeding with implementation based on these policies, we will develop an efficient and reliable Ray.Aop PECL extension. \ No newline at end of file diff --git a/docs/review.md b/docs/review.md deleted file mode 100644 index 7c224a0..0000000 --- a/docs/review.md +++ /dev/null @@ -1,52 +0,0 @@ -# Ray.Aop PECL拡張 仕様書とポリシーのレビュー - -## 評価対象 -- spec.md (仕様書) -- policy.md (実装方針) - -## 評価基準 - -1. 一貫性 -2. 明確性 -3. 詳細度 -4. 網羅性 -5. 将来性 - -## 評価結果 - -### 1. 一貫性 -両文書は概念と用語使用において一貫性があり、「インターセプトハンドラー」という新しい用語を適切に反映しています。 - -### 2. 明確性 -仕様と実装方針が明確に分離されており、それぞれの役割が明確です。 - -### 3. 詳細度 -- spec.md: 具体的なインターフェース、メソッド、使用例を提供しており、開発者が拡張を使用する際の指針となります。 -- policy.md: 実装に関する高レベルの方針を示しており、開発チームの指針となります。 - -### 4. 網羅性 -主要な機能、インターフェース、使用方法がカバーされています。 - -### 5. 将来性 -PHP8のサポートや将来的な拡張性について言及されており、長期的な視点が含まれています。 - -## 現段階での評価 - -### 必要十分性 -現段階では、これらの文書は開発を開始するための必要十分な情報を提供していると考えられます。 - -### 改善の余地 -以下の点について、より詳細な情報を追加することで、文書の完成度をさらに高めることができます: - -- エラーハンドリングの詳細 -- パフォーマンス最適化の具体的な戦略 -- テスト戦略 -- バージョニング方針 - -### 次のステップ -- 具体的な実装計画の作成 -- プロトタイプの開発 -- コードレビュープロセスの確立 - -## 結論 -現段階では、spec.mdとpolicy.mdは開発を開始するための十分な基盤を提供していると評価できます。ただし、開発が進むにつれて、より詳細な情報や新たな要件が明らかになる可能性があるため、これらの文書は living documents として、継続的に更新・改善していくことが重要です。 \ No newline at end of file diff --git a/docs/spec.md b/docs/spec.md index 7e58059..0c2ab17 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -1,50 +1,66 @@ -### Ray.Aop PECL拡張仕様書 +### Ray.Aop PECL Extension Specification -#### 概要 -Ray.Aopは、PHPにアスペクト指向プログラミング(AOP)の機能を提供するPECL拡張です。本仕様書では、Ray.Aop拡張の機能、使用方法、インターフェイスについて説明します。 +#### Overview +Ray.Aop is a PECL extension that provides Aspect-Oriented Programming (AOP) capabilities to PHP. This specification outlines the features, usage, and interfaces of the Ray.Aop extension. -#### 機能 -1. **インターセプション機能**: - 指定されたクラスとメソッドに対して実行時に指定した`intercept`メソッドがコールされます。 +#### Features +1. **Interception Mechanism**: + The `intercept` method of the specified intercept handler is called at runtime for designated classes and methods. -2. **インターセプトハンドラー**: - インターセプトハンドラーがインターセプターを呼び出します。ここをユーザーが作成することでRay.Aop以外のインターセプターとの連携が可能です。 + 2. **Intercept Handlers**: + Intercept handlers are used to insert custom logic before and after method execution. -#### インターフェイスとクラス +#### Interface -##### InterceptHandlerInterface +##### MethodInterceptorInterface -Ray.Aopは、インターセプションを管理するためのインターフェイスを提供します。このインターフェイスは、PHPのユーザーが実装し、特定のクラスとメソッドに対してカスタムロジックを挿入するために使用されます。 +Ray.Aop provides an interface for managing interception. This interface should be implemented by PHP users to insert custom logic for specific classes and methods. - **Namespace**: `Ray\Aop` -- **メソッド**: - - `intercept(object $object, string $method, array $params): mixed` + - **Method**: + - `intercept(object $object, string $method, array $params): mixed` -##### MethodInvocationクラス -インターセプターが呼び出されたときに、対象のメソッドを呼び出し、インターセプターのチェインを管理するためのクラスです。 +#### Function -- **Namespace**: `Ray\Aop` -- **メソッド**: - - `proceed()`: インターセプターのチェインを進め、最終的にターゲットメソッドを実行します。 +##### method_intercept +Registers an intercept handler for the specified class and method. -#### 関数 +- **Function Name**: `method_intercept` + - **Parameters**: + - `string $className`: Target class name + - `string $methodName`: Target method name + - `Ray\Aop\MethodInterceptorInterface $handler`: Intercept handler to register + - **Return Value**: `bool` (true if registration is successful, false if it fails) -##### method_intercept -指定されたクラスとメソッドに対してインターセプトハンドラーを登録します。 +#### Usage -- **関数名**: `method_intercept` -- **パラメータ**: - - `string $className`: 対象クラス名 - - `string $methodName`: 対象メソッド名 - - `Ray\Aop\InterceptHandlerInterface $handler`: 登録するインターセプトハンドラー -- **戻り値**: なし (`void`) +1. **Implementing an Intercept Handler**: + Create a class that implements `Ray\Aop\MethodInterceptorInterface`. -#### 使用方法 +```php +myMethod(); // This call will trigger the interceptor +``` + +#### Important Notes + +- The intercept handler is executed every time the target method is called. + - If multiple intercept handlers are registered for the same method, only the last registered one will be effective. + - When calling the original method within an intercept handler, always use `call_user_func_array` or `call_user_func`. + - The extension does not currently support method invocation chaining or multiple interceptors per method. + - Interceptors are applied globally and affect all instances of the intercepted class. \ No newline at end of file diff --git a/include/php_rayaop.h b/include/php_rayaop.h deleted file mode 100644 index b6d7a34..0000000 --- a/include/php_rayaop.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef PHP_RAYAOP_H -#define PHP_RAYAOP_H - -extern zend_module_entry rayaop_module_entry; // 拡張機能のエントリポイント -#define phpext_rayaop_ptr &rayaop_module_entry // 拡張機能のポインタ - -#define PHP_RAYAOP_VERSION "1.0.0" // 拡張機能のバージョン - -#ifdef ZTS -#include "TSRM.h" // スレッドセーフリソース管理ヘッダー -#endif - -/** - * 拡張機能のグローバル変数の宣言 - * @link https://www.php.net/manual/en/internals2.variables.globals.php - */ -ZEND_BEGIN_MODULE_GLOBALS(rayaop) - // 拡張機能のグローバル変数をここに追加 -ZEND_END_MODULE_GLOBALS(rayaop) - -/** - * グローバル変数アクセス用のマクロ - * @link https://www.php.net/manual/en/internals2.variables.globals.php - */ -#define RAYAOP_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(rayaop, v) - -/** - * スレッドセーフリソース管理のキャッシュの外部宣言 - * @link https://www.php.net/manual/en/internals2.variables.globals.php - */ -#if defined(ZTS) && defined(COMPILE_DL_RAYAOP) -ZEND_TSRMLS_CACHE_EXTERN() -#endif - -PHP_FUNCTION(method_intercept); // method_intercept関数の宣言 - -#endif /* PHP_RAYAOP_H */ diff --git a/php_rayaop.h b/php_rayaop.h new file mode 100644 index 0000000..b9a6175 --- /dev/null +++ b/php_rayaop.h @@ -0,0 +1,91 @@ +#ifndef PHP_RAYAOP_H // PHP_RAYAOP_Hが定義されていない場合 +#define PHP_RAYAOP_H // PHP_RAYAOP_Hを定義(ヘッダーガード) + +#ifdef HAVE_CONFIG_H // HAVE_CONFIG_Hが定義されている場合 +#include "config.h" // config.hをインクルード +#endif + +#include "php.h" // PHP核のヘッダーファイルをインクルード +#include "php_ini.h" // PHP INI関連のヘッダーファイルをインクルード +#include "ext/standard/info.h" // 標準拡張モジュールの情報関連ヘッダーをインクルード +#include "zend_exceptions.h" // Zend例外処理関連のヘッダーをインクルード +#include "zend_interfaces.h" // Zendインターフェース関連のヘッダーをインクルード + +#ifdef ZTS // スレッドセーフモードの場合 +#include "TSRM.h" // Thread Safe Resource Managerをインクルード +#endif + +#define PHP_RAYAOP_VERSION "1.0.0" // RayAOP拡張機能のバージョンを定義 +#define RAYAOP_NS "Ray\\Aop\\" // RayAOPの名前空間を定義 + +extern zend_module_entry rayaop_module_entry; // rayaopモジュールエントリを外部参照として宣言 +#define phpext_rayaop_ptr &rayaop_module_entry // rayaopモジュールへのポインタを定義 + +#ifdef PHP_WIN32 // Windows環境の場合 +#define PHP_RAYAOP_API __declspec(dllexport) // DLLエクスポート指定 +#elif defined(__GNUC__) && __GNUC__ >= 4 // GCC 4以上の場合 +#define PHP_RAYAOP_API __attribute__ ((visibility("default"))) // デフォルトの可視性を指定 +#else // その他の環境 +#define PHP_RAYAOP_API // 特に指定なし +#endif + +#ifdef ZTS // スレッドセーフモードの場合 +#include "TSRM.h" // Thread Safe Resource Managerを再度インクルード(冗長だが安全のため) +#endif + +// デバッグ出力用マクロ +#ifdef RAYAOP_DEBUG // デバッグモードが有効な場合 +#define RAYAOP_DEBUG_PRINT(fmt, ...) php_printf("RAYAOP DEBUG: " fmt "\n", ##__VA_ARGS__) // デバッグ出力マクロを定義 +#else // デバッグモードが無効な場合 +#define RAYAOP_DEBUG_PRINT(fmt, ...) // 何もしない +#endif + +// エラーコード +#define RAYAOP_ERROR_MEMORY_ALLOCATION 1 // メモリ割り当てエラーのコード +#define RAYAOP_ERROR_HASH_UPDATE 2 // ハッシュ更新エラーのコード + +/** + * インターセプト情報を保持する構造体 + * link: http//://www.phpinternalsbook.com/php5/classes_objects/internal_structures_and_implementation.html + * link: http://php.adamharvey.name/manual/ja/internals2.variables.tables.php + */ +typedef struct _intercept_info { + zend_string *class_name; // インターセプト対象のクラス名 + zend_string *method_name; // インターセプト対象のメソッド名 + zval handler; // インターセプトハンドラー +} intercept_info; + +// 関数宣言 +PHP_MINIT_FUNCTION(rayaop); // モジュール初期化関数 +PHP_MSHUTDOWN_FUNCTION(rayaop); // モジュールシャットダウン関数 +PHP_RINIT_FUNCTION(rayaop); // リクエスト初期化関数 +PHP_RSHUTDOWN_FUNCTION(rayaop); // リクエストシャットダウン関数 +PHP_MINFO_FUNCTION(rayaop); // モジュール情報関数 + +PHP_FUNCTION(method_intercept); // メソッドインターセプト関数 + +// ユーティリティ関数の宣言 +void rayaop_handle_error(const char *message); // エラーハンドリング関数 +bool rayaop_should_intercept(zend_execute_data *execute_data); // インターセプトの必要性を判断する関数 +char* rayaop_generate_intercept_key(zend_string *class_name, zend_string *method_name, size_t *key_len); // インターセプトキーを生成する関数 +intercept_info* rayaop_find_intercept_info(const char *key, size_t key_len); // インターセプト情報を検索する関数 +void rayaop_execute_intercept(zend_execute_data *execute_data, intercept_info *info); // インターセプトを実行する関数 +void rayaop_free_intercept_info(zval *zv); // インターセプト情報を解放する関数 + +#ifdef RAYAOP_DEBUG // デバッグモードが有効な場合 +void rayaop_debug_print_zval(zval *value); // zval値をデバッグ出力する関数 +void rayaop_dump_intercept_info(void); // インターセプト情報をダンプする関数 +#endif + +ZEND_BEGIN_MODULE_GLOBALS(rayaop) // rayaopモジュールのグローバル変数の開始 + HashTable *intercept_ht; // インターセプトハッシュテーブル + zend_bool is_intercepting; // インターセプト中フラグ +ZEND_END_MODULE_GLOBALS(rayaop) // rayaopモジュールのグローバル変数の終了 + +#ifdef ZTS // スレッドセーフモードの場合 +#define RAYAOP_G(v) TSRMG(rayaop_globals_id, zend_rayaop_globals *, v) // グローバル変数アクセスマクロ(スレッドセーフ版) +#else // 非スレッドセーフモードの場合 +#define RAYAOP_G(v) (rayaop_globals.v) // グローバル変数アクセスマクロ(非スレッドセーフ版) +#endif + +#endif /* PHP_RAYAOP_H */ // ヘッダーガードの終了 \ No newline at end of file diff --git a/rayaop.c b/rayaop.c new file mode 100644 index 0000000..24b9656 --- /dev/null +++ b/rayaop.c @@ -0,0 +1,386 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" // 設定ファイルをインクルード +#endif + +#include "php_rayaop.h" // RayAOP拡張機能のヘッダーファイルをインクルード + +// モジュールグローバル変数の宣言 +ZEND_DECLARE_MODULE_GLOBALS(rayaop) + +// 静的変数の宣言:元のzend_execute_ex関数へのポインタ +static void (*original_zend_execute_ex)(zend_execute_data *execute_data); + +/** + * グローバル初期化関数 + * @param zend_rayaop_globals *rayaop_globals グローバル変数へのポインタ + */ +static void php_rayaop_init_globals(zend_rayaop_globals *rayaop_globals) +{ + rayaop_globals->intercept_ht = NULL; // インターセプトハッシュテーブルを初期化 + rayaop_globals->is_intercepting = 0; // インターセプトフラグを初期化 +} + +// デバッグ出力用マクロ +#ifdef RAYAOP_DEBUG +#define RAYAOP_DEBUG_PRINT(fmt, ...) php_printf("RAYAOP DEBUG: " fmt "\n", ##__VA_ARGS__) // デバッグ情報を出力 +#else +#define RAYAOP_DEBUG_PRINT(fmt, ...) // デバッグモードでない場合は何もしない +#endif + +// method_intercept 関数の引数情報 +ZEND_BEGIN_ARG_INFO_EX(arginfo_method_intercept, 0, 0, 3) + ZEND_ARG_TYPE_INFO(0, class_name, IS_STRING, 0) // クラス名の引数情報 + ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) // メソッド名の引数情報 + ZEND_ARG_OBJ_INFO(0, interceptor, Ray\\Aop\\MethodInterceptorInterface, 0) // インターセプトハンドラーの引数情報 +ZEND_END_ARG_INFO() + +// ユーティリティ関数の実装 + +/** + * エラーハンドリング関数 + * @param const char *message エラーメッセージ + */ +void rayaop_handle_error(const char *message) { + php_error_docref(NULL, E_ERROR, "Memory error: %s", message); // エラーメッセージを出力 +} + +/** + * インターセプトが必要かどうかを判断する関数 + * @param zend_execute_data *execute_data 実行データ + * @return bool インターセプトが必要な場合はtrue + */ +bool rayaop_should_intercept(zend_execute_data *execute_data) { + return execute_data->func->common.scope && // スコープが存在し + execute_data->func->common.function_name && // 関数名が存在し + !RAYAOP_G(is_intercepting); // 現在インターセプト中でない +} + +/** + * インターセプトキーを生成する関数 + * @param zend_string *class_name クラス名 + * @param zend_string *method_name メソッド名 + * @param size_t *key_len キーの長さを格納するポインタ + * @return char* 生成されたキー + */ +char* rayaop_generate_intercept_key(zend_string *class_name, zend_string *method_name, size_t *key_len) { + char *key = NULL; + *key_len = spprintf(&key, 0, "%s::%s", ZSTR_VAL(class_name), ZSTR_VAL(method_name)); // クラス名::メソッド名 の形式でキーを生成 + RAYAOP_DEBUG_PRINT("Generated key: %s", key); // 生成されたキーをデバッグ出力 + return key; +} + +/** + * インターセプト情報を検索する関数 + * @param const char *key 検索キー + * @param size_t key_len キーの長さ + * @return intercept_info* 見つかったインターセプト情報、見つからない場合はNULL + */ +intercept_info* rayaop_find_intercept_info(const char *key, size_t key_len) { + return zend_hash_str_find_ptr(RAYAOP_G(intercept_ht), key, key_len); // ハッシュテーブルからインターセプト情報を検索 +} + +/** + * インターセプトを実行する関数 + * @param zend_execute_data *execute_data 実行データ + * @param intercept_info *info インターセプト情報 + */ +void rayaop_execute_intercept(zend_execute_data *execute_data, intercept_info *info) { + if (Z_TYPE(info->handler) != IS_OBJECT) { // ハンドラーがオブジェクトでない場合 + return; // 処理を中断 + } + + if (execute_data->This.value.obj == NULL) { // 対象オブジェクトがNULLの場合 + RAYAOP_DEBUG_PRINT("Object is NULL, calling original function"); // デバッグ情報を出力 + original_zend_execute_ex(execute_data); // 元の関数を実行 + return; // 処理を中断 + } + + zval retval, params[3]; // 戻り値と引数を格納する変数を準備 + ZVAL_OBJ(¶ms[0], execute_data->This.value.obj); // 第1引数:対象オブジェクト + ZVAL_STR(¶ms[1], info->method_name); // 第2引数:メソッド名 + + array_init(¶ms[2]); // 第3引数:メソッドの引数を格納する配列を初期化 + uint32_t arg_count = ZEND_CALL_NUM_ARGS(execute_data); // 引数の数を取得 + zval *args = ZEND_CALL_ARG(execute_data, 1); // 引数の配列を取得 + for (uint32_t i = 0; i < arg_count; i++) { // 各引数に対して + zval *arg = &args[i]; + Z_TRY_ADDREF_P(arg); // 参照カウントを増やす + add_next_index_zval(¶ms[2], arg); // 配列に追加 + } + + RAYAOP_G(is_intercepting) = 1; // インターセプトフラグを設定 + zval func_name; + ZVAL_STRING(&func_name, "intercept"); // 呼び出す関数名を設定 + + ZVAL_UNDEF(&retval); // 戻り値を未定義に初期化 + if (call_user_function(NULL, &info->handler, &func_name, &retval, 3, params) == SUCCESS) { // インターセプト関数を呼び出し + if (!Z_ISUNDEF(retval)) { // 戻り値が定義されている場合 + ZVAL_COPY(execute_data->return_value, &retval); // 戻り値をコピー + } + } else { // インターセプトに失敗した場合 + php_error_docref(NULL, E_WARNING, "Interception failed for %s::%s", ZSTR_VAL(info->class_name), ZSTR_VAL(info->method_name)); // 警告を出力 + } + + zval_ptr_dtor(&retval); // 戻り値のメモリを解放 + zval_ptr_dtor(&func_name); // 関数名のメモリを解放 + zval_ptr_dtor(¶ms[1]); // メソッド名のメモリを解放 + zval_ptr_dtor(¶ms[2]); // 引数配列のメモリを解放 + + RAYAOP_G(is_intercepting) = 0; // インターセプトフラグをリセット +} + +/** + * インターセプト情報を解放する関数 + * @param zval *zv 解放するインターセプト情報を含むzval + */ +void rayaop_free_intercept_info(zval *zv) { + intercept_info *info = Z_PTR_P(zv); // zvalからインターセプト情報ポインタを取得 + if (info) { // インターセプト情報が存在する場合 + RAYAOP_DEBUG_PRINT("Freeing intercept info for %s::%s", ZSTR_VAL(info->class_name), ZSTR_VAL(info->method_name)); // デバッグ情報を出力 + zend_string_release(info->class_name); // クラス名のメモリを解放 + zend_string_release(info->method_name); // メソッド名のメモリを解放 + zval_ptr_dtor(&info->handler); // ハンドラーのメモリを解放 + efree(info); // インターセプト情報構造体のメモリを解放 + } +} + +/** + * カスタム zend_execute_ex 関数 + * @param zend_execute_data *execute_data 実行データ + */ +static void rayaop_zend_execute_ex(zend_execute_data *execute_data) { + RAYAOP_DEBUG_PRINT("rayaop_zend_execute_ex called"); // デバッグ情報を出力 + + if (!rayaop_should_intercept(execute_data)) { // インターセプトが不要な場合 + original_zend_execute_ex(execute_data); // 元の実行関数を呼び出し + return; // 処理を終了 + } + + zend_function *current_function = execute_data->func; // 現在の関数情報を取得 + zend_string *class_name = current_function->common.scope->name; // クラス名を取得 + zend_string *method_name = current_function->common.function_name; // メソッド名を取得 + + size_t key_len; + char *key = rayaop_generate_intercept_key(class_name, method_name, &key_len); // インターセプトキーを生成 + + intercept_info *info = rayaop_find_intercept_info(key, key_len); // インターセプト情報を検索 + + if (info) { // インターセプト情報が見つかった場合 + RAYAOP_DEBUG_PRINT("Found intercept info for key: %s", key); // デバッグ情報を出力 + rayaop_execute_intercept(execute_data, info); // インターセプトを実行 + } else { // インターセプト情報が見つからなかった場合 + RAYAOP_DEBUG_PRINT("No intercept info found for key: %s", key); // デバッグ情報を出力 + original_zend_execute_ex(execute_data); // 元の実行関数を呼び出し + } + + efree(key); // キーのメモリを解放 +} + +/** + * ハッシュテーブル更新失敗時の処理 + * @param intercept_info *new_info 新しいインターセプト情報 + * @param char *key 更新に使用されたキー + */ +void hash_update_failed(intercept_info *new_info, char *key) { + rayaop_handle_error("Failed to update intercept hash table"); // エラーメッセージを出力 + zend_string_release(new_info->class_name); // クラス名のメモリを解放 + zend_string_release(new_info->method_name); // メソッド名のメモリを解放 + zval_ptr_dtor(&new_info->handler); // ハンドラーのメモリを解放 + efree(new_info); // インターセプト情報構造体のメモリを解放 + efree(key); // キーのメモリを解放 +} + +/** + * インターセプトメソッドを登録する関数 + * link: https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html + * + * @param INTERNAL_FUNCTION_PARAMETERS 内部関数パラメータ + */ +PHP_FUNCTION(method_intercept) { + RAYAOP_DEBUG_PRINT("method_intercept called"); // デバッグ情報を出力 + + char *class_name, *method_name; // クラス名とメソッド名のポインタ + size_t class_name_len, method_name_len; // クラス名とメソッド名の長さ + zval *intercepted; // インターセプトハンドラー + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_STRING(class_name, class_name_len) // クラス名のパラメータを解析 + Z_PARAM_STRING(method_name, method_name_len) // メソッド名のパラメータを解析 + Z_PARAM_OBJECT(intercepted) // インターセプトハンドラーのパラメータを解析 + ZEND_PARSE_PARAMETERS_END(); + + intercept_info *new_info = ecalloc(1, sizeof(intercept_info)); // 新しいインターセプト情報のメモリを確保 + if (!new_info) { // メモリ確保に失敗した場合 + rayaop_handle_error("Failed to allocate memory for intercept_info"); // エラーメッセージを出力 + RETURN_FALSE; // falseを返して終了 + } + +new_info->class_name = zend_string_init(class_name, class_name_len, 0); // クラス名を初期化 + new_info->method_name = zend_string_init(method_name, method_name_len, 0); // メソッド名を初期化 + ZVAL_COPY(&new_info->handler, intercepted); // インターセプトハンドラーをコピー + + char *key = NULL; + size_t key_len = spprintf(&key, 0, "%s::%s", class_name, method_name); // インターセプトキーを生成 + + if (zend_hash_str_update_ptr(RAYAOP_G(intercept_ht), key, key_len, new_info) == NULL) { // ハッシュテーブルに追加 + hash_update_failed(new_info, key); // 追加に失敗した場合、エラー処理を実行 + RETURN_FALSE; // falseを返して終了 + } + + efree(key); // キーのメモリを解放 + RAYAOP_DEBUG_PRINT("Successfully registered intercept info"); // デバッグ情報を出力 + RETURN_TRUE; // trueを返して終了 +} + +// インターフェースの定義 +zend_class_entry *ray_aop_method_interceptor_interface_ce; + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_ray_aop_method_interceptor_intercept, 0, 3, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) // 第1引数:オブジェクト + ZEND_ARG_TYPE_INFO(0, method, IS_STRING, 0) // 第2引数:メソッド名 + ZEND_ARG_TYPE_INFO(0, params, IS_ARRAY, 0) // 第3引数:パラメータ配列 +ZEND_END_ARG_INFO() + +static const zend_function_entry ray_aop_method_interceptor_interface_methods[] = { + ZEND_ABSTRACT_ME(Ray_Aop_MethodInterceptorInterface, intercept, arginfo_ray_aop_method_interceptor_intercept) // interceptメソッドを定義 + PHP_FE_END // 関数エントリの終了 +}; + +/** + * 拡張機能の初期化関数 + * @param INIT_FUNC_ARGS 初期化関数の引数 + * @return int 初期化の成功・失敗 + */ +PHP_MINIT_FUNCTION(rayaop) +{ + RAYAOP_DEBUG_PRINT("PHP_MINIT_FUNCTION called"); // デバッグ情報を出力 + +#ifdef ZTS + ts_allocate_id(&rayaop_globals_id, sizeof(zend_rayaop_globals), (ts_allocate_ctor) php_rayaop_init_globals, NULL); // スレッドセーフモードでのグローバル変数の初期化 +#else + php_rayaop_init_globals(&rayaop_globals); // 非スレッドセーフモードでのグローバル変数の初期化 +#endif + + zend_class_entry ce; + INIT_CLASS_ENTRY(ce, "Ray\\Aop\\MethodInterceptorInterface", ray_aop_method_interceptor_interface_methods); // クラスエントリを初期化 + ray_aop_method_interceptor_interface_ce = zend_register_internal_interface(&ce); // インターフェースを登録 + + original_zend_execute_ex = zend_execute_ex; // 元のzend_execute_ex関数を保存 + zend_execute_ex = rayaop_zend_execute_ex; // カスタムのzend_execute_ex関数を設定 + + RAYAOP_DEBUG_PRINT("RayAOP extension initialized"); // デバッグ情報を出力 + return SUCCESS; // 成功を返す +} + +/** + * 拡張機能のシャットダウン関数 + * @param SHUTDOWN_FUNC_ARGS シャットダウン関数の引数 + * @return int シャットダウンの成功・失敗 + */ +PHP_MSHUTDOWN_FUNCTION(rayaop) +{ + RAYAOP_DEBUG_PRINT("RayAOP PHP_MSHUTDOWN_FUNCTION called"); // デバッグ情報を出力 + + zend_execute_ex = original_zend_execute_ex; // 元のzend_execute_ex関数を復元 + original_zend_execute_ex = NULL; // 保存していたポインタをクリア + + RAYAOP_DEBUG_PRINT("RayAOP PHP_MSHUTDOWN_FUNCTION shut down"); // デバッグ情報を出力 + return SUCCESS; // シャットダウン成功 +} + +/** + * リクエスト開始時の初期化関数 + * @param INIT_FUNC_ARGS 初期化関数の引数 + * @return int 初期化の成功・失敗 + */ +PHP_RINIT_FUNCTION(rayaop) +{ + RAYAOP_DEBUG_PRINT("PHP_RINIT_FUNCTION called"); // デバッグ情報を出力 + + if (RAYAOP_G(intercept_ht) == NULL) { // インターセプトハッシュテーブルが未初期化の場合 + ALLOC_HASHTABLE(RAYAOP_G(intercept_ht)); // ハッシュテーブルのメモリを確保 + zend_hash_init(RAYAOP_G(intercept_ht), 8, NULL, rayaop_free_intercept_info, 0); // ハッシュテーブルを初期化 + } + RAYAOP_G(is_intercepting) = 0; // インターセプトフラグを初期化 + + return SUCCESS; // 成功を返す +} + +/** + * リクエスト終了時のシャットダウン関数 + * @param SHUTDOWN_FUNC_ARGS シャットダウン関数の引数 + * @return int シャットダウンの成功・失敗 + */ +PHP_RSHUTDOWN_FUNCTION(rayaop) +{ + RAYAOP_DEBUG_PRINT("RayAOP PHP_RSHUTDOWN_FUNCTION called"); // デバッグ情報を出力 + if (RAYAOP_G(intercept_ht)) { // インターセプトハッシュテーブルが存在する場合 + zend_hash_destroy(RAYAOP_G(intercept_ht)); // ハッシュテーブルを破棄 + FREE_HASHTABLE(RAYAOP_G(intercept_ht)); // ハッシュテーブルのメモリを解放 + RAYAOP_G(intercept_ht) = NULL; // ハッシュテーブルポインタをNULLに設定 + } + + RAYAOP_DEBUG_PRINT("RayAOP PHP_RSHUTDOWN_FUNCTION shut down"); // デバッグ情報を出力 + return SUCCESS; // シャットダウン成功 +} + +/** + * 拡張機能の情報表示関数 + * @param ZEND_MODULE_INFO_FUNC_ARGS 情報表示関数の引数 + */ +PHP_MINFO_FUNCTION(rayaop) +{ + php_info_print_table_start(); // 情報テーブルの開始 + php_info_print_table_header(2, "rayaop support", "enabled"); // テーブルヘッダーの表示 + php_info_print_table_row(2, "Version", PHP_RAYAOP_VERSION); // バージョン情報の表示 + php_info_print_table_end(); // 情報テーブルの終了 +} + +// デバッグ用:インターセプト情報をダンプする関数 +#ifdef RAYAOP_DEBUG +/** + * インターセプト情報をダンプする関数(デバッグ用) + */ +static void rayaop_dump_intercept_info(void) +{ + RAYAOP_DEBUG_PRINT("Dumping intercept information:"); // デバッグ情報を出力 + if (RAYAOP_G(intercept_ht)) { // インターセプトハッシュテーブルが存在する場合 + zend_string *key; + intercept_info *info; + ZEND_HASH_FOREACH_STR_KEY_PTR(RAYAOP_G(intercept_ht), key, info) { // ハッシュテーブルの各要素に対して + if (key && info) { // キーと情報が存在する場合 + RAYAOP_DEBUG_PRINT("Key: %s", ZSTR_VAL(key)); // キーを出力 + RAYAOP_DEBUG_PRINT(" Class: %s", ZSTR_VAL(info->class_name)); // クラス名を出力 + RAYAOP_DEBUG_PRINT(" Method: %s", ZSTR_VAL(info->method_name)); // メソッド名を出力 + RAYAOP_DEBUG_PRINT(" Handler type: %d", Z_TYPE(info->handler)); // ハンドラーの型を出力 + } + } ZEND_HASH_FOREACH_END(); + } else { // インターセプトハッシュテーブルが存在しない場合 + RAYAOP_DEBUG_PRINT("Intercept hash table is not initialized"); // 初期化されていないことを出力 + } +} +#endif + +// 拡張機能が提供する関数の定義 +static const zend_function_entry rayaop_functions[] = { + PHP_FE(method_intercept, arginfo_method_intercept) // method_intercept関数の登録 + PHP_FE_END // 関数エントリの終了 +}; + +// 拡張機能のモジュールエントリ +zend_module_entry rayaop_module_entry = { + STANDARD_MODULE_HEADER, + "rayaop", // 拡張機能の名前 + rayaop_functions, // 拡張機能が提供する関数 + PHP_MINIT(rayaop), // 拡張機能の初期化関数 + PHP_MSHUTDOWN(rayaop), // 拡張機能のシャットダウン関数 + PHP_RINIT(rayaop), // リクエスト開始時の関数 + PHP_RSHUTDOWN(rayaop), // リクエスト終了時の関数 + PHP_MINFO(rayaop), // 拡張機能の情報表示関数 + PHP_RAYAOP_VERSION, // 拡張機能のバージョン + STANDARD_MODULE_PROPERTIES +}; + +#ifdef COMPILE_DL_RAYAOP +ZEND_GET_MODULE(rayaop) // 動的ロード時のモジュール取得関数 +#endif diff --git a/rayaop.php b/smoke.php similarity index 84% rename from rayaop.php rename to smoke.php index a9bd42b..d4f5e42 100644 --- a/rayaop.php +++ b/smoke.php @@ -1,8 +1,6 @@ func; // 現在の関数情報を取得 - - if (current_function->common.scope && current_function->common.function_name) { - zend_string *class_name = current_function->common.scope->name; // クラス名を取得 - zend_string *method_name = current_function->common.function_name; // メソッド名を取得 - - char *key = NULL; // ハッシュキー用の文字列ポインタ - size_t key_len = 0; // ハッシュキーの長さ - key_len = spprintf(&key, 0, "%s::%s", ZSTR_VAL(class_name), ZSTR_VAL(method_name)); // ハッシュキーを生成 - RAYAOP_DEBUG_PRINT("Generated key: %s", key); - - intercept_info *info = zend_hash_str_find_ptr(intercept_ht, key, key_len); // ハッシュテーブルからインターセプト情報を取得 - - if (info) { - RAYAOP_DEBUG_PRINT("Found intercept info for key: %s", key); - - if (Z_TYPE(info->handler) == IS_OBJECT) { - zval retval, params[3]; // 戻り値とパラメータ用のzvalを宣言 - - if (execute_data->This.value.obj == NULL) { - original_zend_execute_ex(execute_data); // オブジェクトがNULLの場合は元の関数を実行 - return; - } - - ZVAL_OBJ(¶ms[0], execute_data->This.value.obj); // オブジェクトを設定 - ZVAL_STR(¶ms[1], method_name); // メソッド名を設定 - - array_init(¶ms[2]); // 引数の配列を初期化 - uint32_t arg_count = ZEND_CALL_NUM_ARGS(execute_data); // 引数の数を取得 - zval *args = ZEND_CALL_ARG(execute_data, 1); // 引数を取得 - for (uint32_t i = 0; i < arg_count; i++) { - zval *arg = &args[i]; // 各引数を取得 - Z_TRY_ADDREF_P(arg); // 引数の参照カウントを増やす - add_next_index_zval(¶ms[2], arg); // 配列に引数を追加 - } - - is_intercepting = 1; // インターセプト中フラグを設定 - zval func_name; - ZVAL_STRING(&func_name, "intercept"); // インターセプトハンドラーのメソッド名を設定 - - ZVAL_UNDEF(&retval); - if (call_user_function(NULL, &info->handler, &func_name, &retval, 3, params) == SUCCESS) { - if (!Z_ISUNDEF(retval)) { - ZVAL_COPY(execute_data->return_value, &retval); // 戻り値を設定 - } - zval_ptr_dtor(&retval); // 戻り値のデストラクタを呼ぶ - } else { - php_error_docref(NULL, E_WARNING, "Interception failed for %s::%s", ZSTR_VAL(class_name), ZSTR_VAL(method_name)); - } - - zval_ptr_dtor(&func_name); // メソッド名のデストラクタを呼ぶ - zval_ptr_dtor(¶ms[1]); // メソッド名のデストラクタを呼ぶ - zval_ptr_dtor(¶ms[2]); // 引数配列のデストラクタを呼ぶ - - is_intercepting = 0; // インターセプト中フラグを解除 - return; - } - } else { - RAYAOP_DEBUG_PRINT("No intercept info found for key: %s", key); - } - - efree(key); // keyの解放はすべてのデバッグ出力の後 - } - - original_zend_execute_ex(execute_data); // 元のzend_execute_ex関数を呼び出す -} - -ZEND_BEGIN_ARG_INFO_EX(arginfo_method_intercept, 0, 0, 3) - ZEND_ARG_TYPE_INFO(0, class_name, IS_STRING, 0) // クラス名の引数情報 - ZEND_ARG_TYPE_INFO(0, method_name, IS_STRING, 0) // メソッド名の引数情報 - ZEND_ARG_OBJ_INFO(0, interceptor, Ray\\Aop\\MethodInterceptorInterface, 0) // インターセプトハンドラーの引数情報 -ZEND_END_ARG_INFO() - -/** - * インターセプトメソッドを登録する関数 - * @link https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html - */ -PHP_FUNCTION(method_intercept) -{ - RAYAOP_DEBUG_PRINT("method_intercept called"); - - char *class_name, *method_name; // クラス名とメソッド名のポインタ - size_t class_name_len, method_name_len; // クラス名とメソッド名の長さ - zval *intercepted; // インターセプトハンドラー - - ZEND_PARSE_PARAMETERS_START(3, 3) - Z_PARAM_STRING(class_name, class_name_len) // クラス名のパラメータを解析 - Z_PARAM_STRING(method_name, method_name_len) // メソッド名のパラメータを解析 - Z_PARAM_OBJECT(intercepted) // インターセプトハンドラーのパラメータを解析 - ZEND_PARSE_PARAMETERS_END(); - - intercept_info *new_info = ecalloc(1, sizeof(intercept_info)); // 新しいインターセプト情報を確保 - if (!new_info) { - php_error_docref(NULL, E_ERROR, "Memory allocation failed"); // メモリ確保に失敗した場合のエラー - RETURN_FALSE; - } - RAYAOP_DEBUG_PRINT("Allocated memory for intercept_info"); - - new_info->class_name = zend_string_init(class_name, class_name_len, 0); // クラス名を初期化 - new_info->method_name = zend_string_init(method_name, method_name_len, 0); // メソッド名を初期化 - ZVAL_COPY(&new_info->handler, intercepted); // インターセプトハンドラーをコピー - RAYAOP_DEBUG_PRINT("Initialized intercept_info for %s::%s", class_name, method_name); - - char *key = NULL; // ハッシュキー用の文字列ポインタ - size_t key_len = 0; // ハッシュキーの長さ - key_len = spprintf(&key, 0, "%s::%s", class_name, method_name); // ハッシュキーを生成 - RAYAOP_DEBUG_PRINT("Registered intercept info for key: %s", key); - - zend_hash_str_update_ptr(intercept_ht, key, key_len, new_info); // ハッシュテーブルに新しいインターセプト情報を登録 - efree(key); // ハッシュキーを解放 - - RETURN_TRUE; -} - -/** - * インターセプト情報を解放する関数 - * @link https://www.phpinternalsbook.com/php7/internal_types/strings/zend_strings.html - */ -static int efree_intercept_info(zval *zv) -{ - intercept_info *info = Z_PTR_P(zv); // インターセプト情報を取得 - if (info) { - RAYAOP_DEBUG_PRINT("Freeing intercept info for %s::%s", ZSTR_VAL(info->class_name), ZSTR_VAL(info->method_name)); - - // zend_string_release(info->class_name); // クラス名を解放 *SHOULD_BE_REMOVED* - // zend_string_release(info->method_name); // メソッド名を解放 *SHOULD_BE_REMOVED* - - RAYAOP_DEBUG_PRINT("class_name and method_name released"); - - zval_ptr_dtor(&info->handler); // ハンドラーを解放 - RAYAOP_DEBUG_PRINT("handler released"); - - efree(info); // インターセプト情報構造体を解放 - RAYAOP_DEBUG_PRINT("Memory freed for intercept info"); - } - return ZEND_HASH_APPLY_REMOVE; // ハッシュテーブルからエントリを削除 -} - -/** - * 拡張機能の初期化関数 - * @link https://www.phpinternalsbook.com/php7/extensions_design/hooks.html - */ -PHP_MINIT_FUNCTION(rayaop) -{ - RAYAOP_DEBUG_PRINT("PHP_MINIT_FUNCTION called"); - - original_zend_execute_ex = zend_execute_ex; // 元のzend_execute_ex関数を保存 - zend_execute_ex = rayaop_zend_execute_ex; // カスタムzend_execute_ex関数を設定 - - intercept_ht = pemalloc(sizeof(HashTable), 1); // ハッシュテーブルを確保 - zend_hash_init(intercept_ht, 8, NULL, NULL, 1); // ハッシュテーブルを初期化 - - RAYAOP_DEBUG_PRINT("RayAOP extension initialized"); - return SUCCESS; // 初期化成功 -} - -/** - * 拡張機能のシャットダウン関数 - * @link https://www.phpinternalsbook.com/php7/extensions_design/hooks.html - */ -PHP_MSHUTDOWN_FUNCTION(rayaop) -{ - RAYAOP_DEBUG_PRINT("PHP_MSHUTDOWN_FUNCTION called"); - - zend_execute_ex = original_zend_execute_ex; // 元のzend_execute_ex関数を復元 - - if (intercept_ht) { - zend_hash_destroy(intercept_ht); // ハッシュテーブルを破棄 - pefree(intercept_ht, 1); // ハッシュテーブルのメモリを解放 - intercept_ht = NULL; // ハッシュテーブルポインタをNULLに設定 - } - - RAYAOP_DEBUG_PRINT("RayAOP extension shut down"); - return SUCCESS; // シャットダウン成功 -} - -/** - * 拡張機能の情報表示関数 - * @link https://www.phpinternalsbook.com/php7/extensions_design/extension_infos.html - */ -PHP_MINFO_FUNCTION(rayaop) -{ - php_info_print_table_start(); // 情報テーブルの開始 - php_info_print_table_header(2, "rayaop support", "enabled"); // テーブルヘッダーの表示 - php_info_print_table_row(2, "Version", PHP_RAYAOP_VERSION); // バージョン情報の表示 - php_info_print_table_end(); // 情報テーブルの終了 -} - -// https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html -static const zend_function_entry rayaop_functions[] = { - PHP_FE(method_intercept, arginfo_method_intercept) // method_intercept関数の登録 - PHP_FE_END -}; - -// @link https://www.phpinternalsbook.com/php7/extensions_design/extension_infos.html -zend_module_entry rayaop_module_entry = { - STANDARD_MODULE_HEADER, - "rayaop", // 拡張機能の名前 - rayaop_functions, // 拡張機能が提供する関数 - PHP_MINIT(rayaop), // 拡張機能の初期化関数 - PHP_MSHUTDOWN(rayaop), // 拡張機能のシャットダウン関数 - NULL, /* Request init */ - NULL, /* Request shutdown */ - PHP_MINFO(rayaop), // 拡張機能の情報表示関数 - PHP_RAYAOP_VERSION, // 拡張機能のバージョン - STANDARD_MODULE_PROPERTIES -}; - -#ifdef COMPILE_DL_RAYAOP -ZEND_GET_MODULE(rayaop) -#endif diff --git a/tests/000-rayaop-loaded.phpt b/tests/000-rayaop-loaded.phpt new file mode 100644 index 0000000..3f2bf4c --- /dev/null +++ b/tests/000-rayaop-loaded.phpt @@ -0,0 +1,13 @@ +--TEST-- +RayAOP extension is loaded +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/tests/001-rayaop-basic.phpt b/tests/001-rayaop-basic.phpt new file mode 100644 index 0000000..48848fa --- /dev/null +++ b/tests/001-rayaop-basic.phpt @@ -0,0 +1,31 @@ +--TEST-- +RayAOP basic functionality +--FILE-- +$method(...$params); + } +} + +// Register the interceptor +$result = method_intercept(TestClass::class, 'testMethod', new TestInterceptor()); +var_dump($result); + +// Call the intercepted method +$test = new TestClass(); +$result = $test->testMethod("Hello"); +var_dump($result); + +?> +--EXPECTF-- +bool(true) +string(28) "Intercepted: Original: Hello" \ No newline at end of file diff --git a/tests/002-rayaop-error.phpt b/tests/002-rayaop-error.phpt new file mode 100644 index 0000000..9a00a9f --- /dev/null +++ b/tests/002-rayaop-error.phpt @@ -0,0 +1,24 @@ +--TEST-- +RayAOP error handling +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +bool(true) +bool(true) diff --git a/tests/003-rayaop-multiple-interceptors.phpt b/tests/003-rayaop-multiple-interceptors.phpt new file mode 100644 index 0000000..339a480 --- /dev/null +++ b/tests/003-rayaop-multiple-interceptors.phpt @@ -0,0 +1,37 @@ +--TEST-- +RayAOP multiple interceptors +--SKIPIF-- + +--FILE-- +testMethod("Hello"); +var_dump($result); + +?> +--EXPECT-- + string(29) "Interceptor2: Original: Hello" \ No newline at end of file