diff options
-rw-r--r-- | documentation/magic-methods.html | 58 | ||||
-rw-r--r-- | include/base.h | 10 | ||||
-rw-r--r-- | include/classbase.h | 19 | ||||
-rw-r--r-- | src/base.cpp | 19 | ||||
-rw-r--r-- | src/classbase.cpp | 124 |
5 files changed, 209 insertions, 21 deletions
diff --git a/documentation/magic-methods.html b/documentation/magic-methods.html index 0f2067f..a12809a 100644 --- a/documentation/magic-methods.html +++ b/documentation/magic-methods.html @@ -192,15 +192,24 @@ unset($user->email); ?> </code></pre> </p> -<h2>Magic method __call()</h2> +<h2>Magic methods __call() and __invoke()</h2> <p> C++ methods need to be registered explicitly in your extension get_module() - startup function to make them accessible. However, when you override the __call() - method, you can accept all possible method calls. It does not matter what - the name of the method is, when something that looks like a method is called - from PHP user space, it will trigger a call to your __call() method. + startup function to be accessible from PHP user space. However, when you override the __call() + method, you can accept all calls - even calls to methods that do not exist. + When someone makes a call from user space to something that looks like a method, + it will be passed to this __call() method. In a script you can thus use + $object->something(), $object->whatever() or $object->anything() - it does not + matter what the name of the method is, all these calls are passed on to the + __call() method in the C++ class. </p> <p> + Next to the magic __call() function, the PHP-CPP library also supports the + __invoke() method. This is a method that gets called when an object instance + is used <i>as if</i> it was a function. This can be compared with overloading + the operator () in a C++ class. By implementing the __invoke() method, scripts + from PHP user space can create a method, and then use it as a function. +<p> <pre class="language-c++"><code> #include <phpcpp.h> @@ -217,12 +226,22 @@ public: virtual ~MyClass() {} /** - * Override the __call() method to accept all method calls + * Regular method + * @param params Parameters that were passed to the method + * @return Value The return value + */ + Php::Value regular(Php::Parameters &params) + { + return "this is a regular method"; + } + + /** + * Overriden __call() method to accept all method calls * @param name Name of the method that is called * @param params Parameters that were passed to the method * @return Value The return value */ - Php::Value __call(const char *name, Php::Parameters &params) + virtual Php::Value __call(const char *name, Php::Parameters &params) override { // the return value std::string retval = name; @@ -237,6 +256,28 @@ public: // done return retval; } + + /** + * Overridden __invoke() method so that objects can be called directly + * @param params Parameters that were passed to the method + * @return Value The return value + */ + virtual Php::Value __invoke(Php::Parameters &params) override + { + // the return value + std::string retval = "invoke"; + + // loop through the parameters + for (auto &param : params) + { + // append parameter string value to return value + retval += " " + param.stringValue(); + } + + // done + return retval; + } + }; /** @@ -258,6 +299,9 @@ extern "C" { // which methods are accessible Php::Class<MyClass> myClass("MyClass"); + // register the regular method + myClass.method("regular", &MyClass::regular); + // add the class to the extension myExtension.add(std::move(myClass)); diff --git a/include/base.h b/include/base.h index 7e33d8d..ed6936d 100644 --- a/include/base.h +++ b/include/base.h @@ -168,6 +168,16 @@ public: */ virtual Value __call(const char *method, Parameters ¶ms); + /** + * Call the class as if it was a function + * + * This method is called when a an object is used with () operators: + * $object(). You can override this method to make objects callable. + * + * @param params The parameters that were passed to the function + * @return Value The return value + */ + virtual Value __invoke(Parameters ¶ms); private: /** diff --git a/include/classbase.h b/include/classbase.h index f0fe37b..6dba316 100644 --- a/include/classbase.h +++ b/include/classbase.h @@ -352,11 +352,28 @@ private: static int callMethod(const char *method, int ht, struct _zval_struct *return_value, struct _zval_struct **return_value_ptr, struct _zval_struct *this_ptr, int return_value_used); static int callMethod(char *method, int ht, struct _zval_struct *return_value, struct _zval_struct **return_value_ptr, struct _zval_struct *this_ptr, int return_value_used); - + /** + * Method that returns information about the function signature of a undefined method + * @param object_ptr + * @param method + * @param method_len + * @param key + * @return zend_function + */ static union _zend_function *getMethod(struct _zval_struct **object_ptr, char *method, int method_len, const struct _zend_literal *key); static union _zend_function *getMethod(struct _zval_struct **object_ptr, char *method, int method_len); /** + * Method that returns information about the __invoke() method + * @param object + * @param entry + * @param func + * @param object_ptr + * @return int + */ + static int getClosure(struct _zval_struct *object, struct _zend_class_entry **entry, union _zend_function **func, struct _zval_struct **object_ptr); + + /** * Name of the class * @var string */ diff --git a/src/base.cpp b/src/base.cpp index 85cb85c..a5dc7b3 100644 --- a/src/base.cpp +++ b/src/base.cpp @@ -165,6 +165,25 @@ Value Base::__call(const char *method, Parameters ¶ms) } /** + * Call the class as if it was a function + * + * This method is called when a an object is used with () operators: + * $object(). You can override this method to make objects callable. + * + * @param params The parameters that were passed to the function + * @return Value The return value + */ +Value Base::__invoke(Parameters ¶ms) +{ + // throw an exception that will be caught in the ClassBase class, + // so that the default implementation of the function can be called + throw NotImplemented(); + + // unreachable code + return nullptr; +} + +/** * End namespace */ } diff --git a/src/classbase.cpp b/src/classbase.cpp index 2c8efef..00c679a 100644 --- a/src/classbase.cpp +++ b/src/classbase.cpp @@ -53,7 +53,7 @@ static Base *cpp_object(const zval *val) /** * Handler function that runs the __call function - * @param ?? + * @param ... All normal parameters for function calls */ static void call_method(INTERNAL_FUNCTION_PARAMETERS) { @@ -97,6 +97,48 @@ static void call_method(INTERNAL_FUNCTION_PARAMETERS) } /** + * Handler function that runs the __invoke function + * @param ... All normal parameters for function calls + */ +static void call_invoke(INTERNAL_FUNCTION_PARAMETERS) +{ + // retrieve the originally called (and by us allocated) function object + // (this was copied from the zend engine source code, code looks way too + // ugly to be made by me) + zend_internal_function *func = (zend_internal_function *)EG(current_execute_data)->function_state.function; + + // we no longer need it (question: why have we allocated this on the heap???) + efree(func); + + // the function could throw an exception + try + { + // wrap the return value + Value result(return_value, true); + + // construct parameters + Parameters params(this_ptr, ZEND_NUM_ARGS()); + + // retrieve the base object + Base *base = params.object(); + + // call the actual __invoke method on the base object + result = base->__invoke(params); + } + catch (const NotImplemented &exception) + { + // because of the two-step nature, we are going to report the error ourselves + zend_error(E_ERROR, "Function name must be a string"); + + } + catch (Exception &exception) + { + // process the exception + exception.process(); + } +} + +/** * Method that returns the function definition of the __call function * @param object_ptr * @param method_name @@ -116,12 +158,20 @@ zend_function *ClassBase::getMethod(zval **object_ptr, char *method_name, int me // after that, this returned handler is also called. The call_method property // of the object_handlers structure however, never gets called. Typical. + // first we'll check if the default handler does not have an implementation, + // in that case the method is probably already implemented as a regular method +#if PHP_VERSION_ID < 50399 + auto *defaultFunction = std_object_handlers.get_method(object_ptr, method_name, method_len); +#else + auto *defaultFunction = std_object_handlers.get_method(object_ptr, method_name, method_len, key); +#endif + + // did the default implementation do anything? + if (defaultFunction) return defaultFunction; + // retrieve the class entry linked to this object auto *entry = zend_get_class_entry(*object_ptr); - // we need the C++ class meta-information object - ClassBase *meta = cpp_class(entry); - // this is peculiar behavior of the zend engine, we first are going to dynamically // allocate memory holding all the properties of the __call method (we initially // had an implementation here that used a static variable, and that worked too, @@ -131,14 +181,14 @@ zend_function *ClassBase::getMethod(zval **object_ptr, char *method_name, int me auto *function = (zend_internal_function *)emalloc(sizeof(zend_internal_function)); // we're going to set all properties - function->type = ZEND_INTERNAL_FUNCTION; - function->module = nullptr; - function->handler = call_method; - function->arg_info = nullptr; - function->num_args = 0; - function->scope = meta->_entry; - function->fn_flags = ZEND_ACC_CALL_VIA_HANDLER; - function->function_name = method_name; + function->type = ZEND_INTERNAL_FUNCTION; + function->module = nullptr; + function->handler = call_method; + function->arg_info = nullptr; + function->num_args = 0; + function->scope = entry; + function->fn_flags = ZEND_ACC_CALL_VIA_HANDLER; + function->function_name = method_name; // done (cast to zend_function* is allowed, because a zend_function is a union // that has one member being a zend_internal_function) @@ -146,6 +196,53 @@ zend_function *ClassBase::getMethod(zval **object_ptr, char *method_name, int me } /** + * Method that returns the closure -- this is the __invoke handler! + * @param object + * @param entry_ptr + * @param func + * @param object_ptr + * @return int + */ +int ClassBase::getClosure(zval *object, zend_class_entry **entry_ptr, zend_function **func, zval **object_ptr) +{ + // it is really unbelievable how the Zend engine manages to implement every feature + // in a complete different manner. You would expect the __invoke() and the + // __call() functions not to be very different from each other. However, they + // both have a completely different API. This getClosure method is supposed + // to fill the function parameter with all information about the invoke() + // method that is going to get called + + // retrieve the class entry linked to this object + auto *entry = zend_get_class_entry(object); + + // just like we did for getMethod(), we're going to dynamically allocate memory + // with all information about the function + auto *function = (zend_internal_function *)emalloc(sizeof(zend_internal_function)); + + // we're going to set all properties + function->type = ZEND_INTERNAL_FUNCTION; + function->module = nullptr; + function->handler = call_invoke; + function->arg_info = nullptr; + function->num_args = 0; + function->scope = entry; + function->fn_flags = ZEND_ACC_CALL_VIA_HANDLER; + function->function_name = nullptr; + + // assign this dynamically allocated variable to the func parameter + // the case is ok, because zend_internal_function is a member of the + // zend_function union + *func = (zend_function *)function; + + // the object_ptr should be filled with the object on which the method is + // called (otherwise the Zend engine tries to call the method statically) + *object_ptr = object; + + // done + return SUCCESS; +}; + +/** * Retrieve pointer to our own object handlers * @return zend_object_handlers */ @@ -181,8 +278,9 @@ zend_object_handlers *ClassBase::objectHandlers() handlers.has_property = &ClassBase::hasProperty; handlers.unset_property = &ClassBase::unsetProperty; - // when a method is called (__call) + // when a method is called (__call and __invoke) handlers.get_method = &ClassBase::getMethod; + handlers.get_closure = &ClassBase::getClosure; // remember that object is now initialized initialized = true; |