📜 ⬆️ ⬇️

Writing extensions for PHP 7 in C ++

I had to write extensions in order to use the functions of C ++ libraries in PHP code. Also, one heavy extension ported from version 5 to version 7.

If you google the documentation on the topic of writing extensions for PHP, then basically it will be texts up to 2014, relevant for version 5. The php.net site itself provides broken and outdated information , and what you can find in their wiki, again, 5th version. The maximum that was found on the off site is a poor mana on migration of already written extensions.

As a result, the only more or less clear mana for writing extensions for me was PHP source code, which I was guided by when writing and migrating extensions.

In fact, the PHP API has changed so much that even the most detailed articles, such as Wrapping C ++ Classes in a PHP Extension, do not really help when writing extensions for PHP 7.
')
This article discusses working under Linux, I have Kubuntu. For Windows, you need to write other config files, and since Windows is not installed in the extension, and expanding PHP under Windows is not a thankful task, I didn’t understand it.

What do you need


php-dev, gcc, php source codes. A combo for installing everything you need to build from source codes, you can easily google.

Identify faces in photos


To determine the faces we use the library OpenCV, tested on versions> = 2.3.1

The starting point for creating extensions is the ext_skel utility. It allows you to create a blank for a new expansion. We will edit the code that came out after executing this command.

You need to go to the folder /ext source code PHP and from there run
ext_skel with the name of the new extension:

 ./ext_skel --extname=phpcv 

After that, the newly created folder phpcv can be moved somewhere in a more convenient place. We need the tests folder and the files config.m4 , php_phpcv.h and phpcv.c . The phpcv.c file phpcv.c immediately renamed to phpcv.cpp .

config.m4


This is the configuration file used by the phpize utility to prepare our extension for compiling.

The file is a kind of bash script using special macros. These macros are defined in the acinclude.m4 and aclocal.m4 in php source code, and are written in the language of parentheses and punctuation marks. In fact, it is enough to read comments that start with the lines “dnl” and it will be more or less clear what these macros do.

We delete the superfluous, we correct the code for our needs.

config.m4
 PHP_ARG_ENABLE(phpcv, whether to enable phpcv support, [ --enable-phpcv Enable phpcv support]) if test "$PHP_PHPCV" != "no"; then PHP_REQUIRE_CXX() SEARCH_PATH="/usr/local /usr /opt/local" SEARCH_FOR="/include/opencv2/opencv.hpp" if test -r $PHP_PHPCV/$SEARCH_FOR; then CV_DIR=$PHP_PHPCV else AC_MSG_CHECKING([for opencv in default path]) for i in $SEARCH_PATH ; do if test -r $i/$SEARCH_FOR; then CV_DIR=$i AC_MSG_RESULT(found in $i) break fi done fi if test -z "$CV_DIR"; then AC_MSG_RESULT([not found]) AC_MSG_ERROR([Please reinstall the OpenCV distribution]) fi AC_CHECK_HEADER([$CV_DIR/include/opencv2/objdetect/objdetect.hpp], [], AC_MSG_ERROR('opencv2/objdetect/objdetect.hpp' header not found)) AC_CHECK_HEADER([$CV_DIR/include/opencv2/highgui/highgui.hpp], [], AC_MSG_ERROR('opencv2/highgui/highgui.hpp' header not found)) PHP_ADD_LIBRARY_WITH_PATH(opencv_objdetect, $CV_DIR/lib, PHPCV_SHARED_LIBADD) PHP_ADD_LIBRARY_WITH_PATH(opencv_highgui, $CV_DIR/lib, PHPCV_SHARED_LIBADD) PHP_ADD_LIBRARY_WITH_PATH(opencv_imgproc, $CV_DIR/lib, PHPCV_SHARED_LIBADD) PHP_SUBST(PHPCV_SHARED_LIBADD) PHP_NEW_EXTENSION(phpcv, phpcv.cpp, $ext_shared,, -std=c++0x -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) fi 


PHP_ARG_ENABLE - set up a flag with which the extension can be turned on or off during the PHP build process from source codes.
PHP_REQUIRE_CXX () - necessary if we are going to use C ++
Next, the bash code for opencv search.
AC_CHECK_HEADER - we check the availability of the header files we need
PHP_ADD_LIBRARY_WITH_PATH - we include shared libraries
PHP_SUBST - this is necessary to form a make file
PHP_NEW_EXTENSION - the extension name is indicated here, the * .cpp files that participate in the build process are listed, the compiler flags are indicated.

php_phpcv.h


php_phpcv.h
 #ifndef PHP_PHPCV_H #define PHP_PHPCV_H #define PHP_PHPCV_EXTNAME "phpcv" #define PHP_PHPCV_VERSION "0.2.0" #ifdef HAVE_CONFIG_H #include "config.h" #endif extern "C" { #include "php.h" #include "ext/standard/info.h" } #ifdef ZTS #include "TSRM.h" #endif extern zend_module_entry phpcv_module_entry; #define phpext_phpcv_ptr &phpcv_module_entry #if defined(ZTS) && defined(COMPILE_DL_PHPCV) ZEND_TSRMLS_CACHE_EXTERN(); #endif #endif /* PHP_PHPCV_H */ 


Here you should pay attention to the design

extern "C" { ... }
This is necessary for the compatibility of our C ++ code with C PHP code.

phpcv.cpp


Here, finally, there will be C ++ code.
To find faces, we will use the cv :: CascadeClassifier :: detectMultiScale () method.

The extension will provide a single function, here is its prototype:

 /** * @see cv::CascadeClassifier::detectMultiScale() * @param string $imgPath * @param string $cascadePath * @param double $scaleFactor * @param int $minNeighbors * * @return array */ function cv_detect_multiscale($imgPath, $cascadePath, $scaleFactor, $minNeighbors) { } 

phpcv.cpp
 #include "php_phpcv.h" #include <opencv2/objdetect/objdetect.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> PHP_MINFO_FUNCTION(phpcv) { php_info_print_table_start(); php_info_print_table_header(2, "phpcv support", "enabled"); php_info_print_table_end(); } PHP_FUNCTION(cv_detect_multiscale) { char *imgPath = NULL, *cascadePath = NULL; long imgPathLen, cascadePathLen, minNeighbors; double scaleFactor, minWidth, minHeight; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ssdl", &imgPath, &imgPathLen, &cascadePath, &cascadePathLen, &scaleFactor, &minNeighbors) == FAILURE) { RETURN_FALSE; } // Read Image cv::Mat image; image = cv::imread(imgPath, CV_LOAD_IMAGE_GRAYSCALE); if (image.empty()) { RETURN_FALSE; } equalizeHist(image, image); //min size for detected object, discarding objects smaller than this minWidth = image.size().width / 10; minHeight = image.size().height / 10; // Load Face cascade (.xml file) cv::CascadeClassifier faceCascade; if (!faceCascade.load(cascadePath)) { RETURN_FALSE; } // Detect faces std::vector<cv::Rect> faces; faceCascade.detectMultiScale(image, faces, scaleFactor, minNeighbors, 0, cv::Size(minWidth, minHeight)); array_init(return_value); // Build array to return for ( int i = 0; i < faces.size(); i++ ) { // Now we have: faces[i].x faces[i].y faces[i].width faces[i].height zval face; array_init(&face); add_assoc_long(&face, "x", faces[i].x); add_assoc_long(&face, "y", faces[i].y); add_assoc_long(&face, "w", faces[i].width); add_assoc_long(&face, "h", faces[i].height); add_next_index_zval(return_value, &face); } } const zend_function_entry phpcv_functions[] = { PHP_FE(cv_detect_multiscale, NULL) PHP_FE_END }; zend_module_entry phpcv_module_entry = { STANDARD_MODULE_HEADER, PHP_PHPCV_EXTNAME, phpcv_functions, NULL, NULL, NULL, NULL, PHP_MINFO(phpcv), PHP_PHPCV_VERSION, STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_PHPCV #ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE(); #endif ZEND_GET_MODULE(phpcv) #endif 


PHP_MINFO_FUNCTION - adds information about our extension to the output of phpinfo ()
PHP_FUNCTION (cv_detect_multiscale) is the code of our function. After receiving the input parameters using zend_parse_parameters C ++ code is in it. Using the opencv library, we find faces and form the output array with the coordinates of the faces found.
zend_function_entry - functions that are provided by the extension are listed here.
zend_module_entry is a standard construct, a structure describing our extension. Several NULL in a row are instead of methods that are executed during initialization and shutdown extensions and queries, we simply have nothing to do during these phases.

In the code you can see two magic dozens. I decided not to bother with the transfer of parameters for the minimum and maximum size of the face in the photo, which must be found.

tests


The extension test consists of code that prints something and checks the output. Files *. Phpt with tests are placed in the folder tests

002.phpt
 --TEST-- Test face detection --SKIPIF-- <?php if (!extension_loaded("phpcv")) print "skip"; ?> --FILE-- <?php $cascade = '/usr/share/opencv/haarcascades/haarcascade_frontalface_alt2.xml'; $img = __DIR__ . '/img.jpg'; $faces = cv_detect_multiscale($img, $cascade, 1.1, 5); if (is_array($faces) && count($faces) > 0) { echo 'face detection works'; } ?> --EXPECT-- face detection works 


Build and Testing


Assemble:

 phpize && ./configure && make 

Test:

 make test 

DateTime Pattern Generator


Now consider an extension that provides a class that wraps the C ++ class.
Add the missing functionality to the intl extension class set. Why it is needed: https://blog.ksimka.io/a-long-journey-to-formatting-a-date-without-a-year-internationally-with-php/#header . In short, the standard intl extension does not provide the ability to internationally form a date without a year, that is, "February 10" or "February 10th." This extension fixes this problem.

The system must have an ICU library installed. On Debian-like systems, you can install the libicu-dev package.

config.m4


config.m4
 PHP_ARG_ENABLE(intl-dtpg, whether to enable intl-dtpg support, [ --enable-intl-dtpg Enable intl-dtpg support]) if test "$PHP_INTL_DTPG" != "no"; then PHP_SETUP_ICU(INTL_DTPG_SHARED_LIBADD) PHP_SUBST(INTL_DTPG_SHARED_LIBADD) PHP_REQUIRE_CXX() PHP_NEW_EXTENSION(intl_dtpg, intl_dtpg.cpp, $ext_shared,,-std=c++0x $ICU_INCS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) fi 


This time everything is very concise. Since we need the ICU library and the intl extension also requires its presence, we simply borrowed the PHP_SETUP_ICU macro from the intl extension.
In PHP_NEW_EXTENSION $ICU_INCS - ICU specific flags.

intl_dtpg.h


intl_dtpg.h
 #ifndef INTL_DTPG_H #define INTL_DTPG_H #include <zend_modules.h> #include <zend_types.h> #include <unicode/dtptngen.h> extern "C" { #include "php.h" #include "ext/standard/info.h" } extern zend_module_entry intl_dtpg_module_entry; #define phpext_intl_dtpg_ptr &intl_dtpg_module_entry #define INTL_DTPG_VERSION "1.0.0" #ifdef ZTS #include "TSRM.h" #endif typedef struct { DateTimePatternGenerator *dtpg; UErrorCode status; zend_object zo; } IntlDateTimePatternGenerator_object; static inline IntlDateTimePatternGenerator_object *php_intl_datetimepatterngenerator_fetch_object(zend_object *obj) { return (IntlDateTimePatternGenerator_object *)((char*)(obj) - XtOffsetOf(IntlDateTimePatternGenerator_object, zo)); } #if defined(ZTS) && defined(COMPILE_DL_INTL_DTPG) ZEND_TSRMLS_CACHE_EXTERN() #endif #endif /* INTL_DTPG_H */ 


Here we define the structure IntlDateTimePatternGenerator_object . It stores a pointer to a DateTimePatternGenerator object from the ICU library, a variable to store the status, and a zend_object object, which is a PHP class. This is such a wrapper for the C ++ class. The PHP API operates with a zend_object object, and we wrapped it in a structure and always have access to what is “next door” to zend_object .

Below we see the definition of the inline function for extracting the IntlDateTimePatternGenerator_object structure with a zend_object object. Under the hood, we take a step back from the start of the zend_object to the size of our structure minus the size of the zend_object . Thus, we find ourselves at the beginning of the structure, the pointer to which we are returned. Such a clever way is peeped in the source codes of the intl extension.

intl_dtpg.cpp


The extension will provide a class of the following structure:

 class IntlDateTimePatternGenerator { /** * @param string $locale */ public function __construct(string $locale) {} /** * Return the best pattern matching the input skeleton. * It is guaranteed to have all of the fields in the skeleton. * * @param string $skeleton The skeleton is a pattern containing only the variable fields. * For example, "MMMdd" and "mmhh" are skeletons. * @return string The best pattern found from the given skeleton. */ public function findBestPattern(string $skeleton) {} } 

intl_dtpg.cpp
 #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "intl_dtpg.h" #include <unicode/ustdio.h> #include <unicode/smpdtfmt.h> zend_class_entry *IntlDateTimePatternGenerator_ce; zend_object_handlers IntlDateTimePatternGenerator_object_handlers; /* {{{ IntlDateTimePatternGenerator_objects_dtor */ static void IntlDateTimePatternGenerator_object_dtor(zend_object *object) { zend_objects_destroy_object(object); } /* }}} */ /* {{{ IntlDateTimePatternGenerator_objects_free */ void IntlDateTimePatternGenerator_object_free(zend_object *object) { IntlDateTimePatternGenerator_object *dtpgo = php_intl_datetimepatterngenerator_fetch_object(object); zend_object_std_dtor(&dtpgo->zo); dtpgo->status = U_ZERO_ERROR; if (dtpgo->dtpg) { delete dtpgo->dtpg; dtpgo->dtpg = nullptr; } } /* }}} */ /* {{{ IntlDateTimePatternGenerator_object_create */ zend_object *IntlDateTimePatternGenerator_object_create(zend_class_entry *ce) { IntlDateTimePatternGenerator_object* intern; intern = (IntlDateTimePatternGenerator_object*)ecalloc(1, sizeof(IntlDateTimePatternGenerator_object) + zend_object_properties_size(ce)); zend_object_std_init(&intern->zo, ce); object_properties_init(&intern->zo, ce); intern->dtpg = nullptr; intern->status = U_ZERO_ERROR; intern->zo.handlers = &IntlDateTimePatternGenerator_object_handlers; return &intern->zo; } /* }}} */ /* {{{ proto void IntlDateTimePatternGenerator::__construct(string $locale) * IntlDateTimePatternGenerator object constructor. */ PHP_METHOD(IntlDateTimePatternGenerator, __construct) { zend_string *locale; zval *object; IntlDateTimePatternGenerator_object* dtpg = nullptr; if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &locale) == FAILURE) { return; } object = getThis(); dtpg = php_intl_datetimepatterngenerator_fetch_object(Z_OBJ_P(object)); dtpg->status = U_ZERO_ERROR; dtpg->dtpg = DateTimePatternGenerator::createInstance(Locale(ZSTR_VAL(locale)), dtpg->status); } /* }}} */ /* {{{ proto string IntlDateTimePatternGenerator::findBestPattern(string $skeleton) * Return the best pattern matching the input skeleton. */ PHP_METHOD(IntlDateTimePatternGenerator, findBestPattern) { zend_string *skeleton; zval *object; IntlDateTimePatternGenerator_object* dtpg = nullptr; if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &skeleton) == FAILURE) { return; } object = getThis(); dtpg = php_intl_datetimepatterngenerator_fetch_object(Z_OBJ_P(object)); UnicodeString pattern = dtpg->dtpg->getBestPattern(UnicodeString(ZSTR_VAL(skeleton)), dtpg->status); std::string s; pattern.toUTF8String(s); RETURN_STRING(s.c_str()); } /* }}} */ ZEND_BEGIN_ARG_INFO_EX(arginfo_findBestPattern, 0, 0, 1) ZEND_ARG_INFO(0, skeleton) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo___construct, 0, 0, 1) ZEND_ARG_INFO(0, locale) ZEND_END_ARG_INFO() const zend_function_entry IntlDateTimePatternGenerator_functions[] = { PHP_ME(IntlDateTimePatternGenerator, __construct, arginfo___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) PHP_ME(IntlDateTimePatternGenerator, findBestPattern, arginfo_findBestPattern, ZEND_ACC_PUBLIC) PHP_FE_END }; /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(intl_dtpg) { zend_class_entry ce; INIT_CLASS_ENTRY(ce, "IntlDateTimePatternGenerator", IntlDateTimePatternGenerator_functions); ce.create_object = IntlDateTimePatternGenerator_object_create; IntlDateTimePatternGenerator_ce = zend_register_internal_class(&ce); memcpy(&IntlDateTimePatternGenerator_object_handlers, zend_get_std_object_handlers(), sizeof IntlDateTimePatternGenerator_object_handlers); IntlDateTimePatternGenerator_object_handlers.offset = XtOffsetOf(IntlDateTimePatternGenerator_object, zo); IntlDateTimePatternGenerator_object_handlers.clone_obj = NULL; //no clone support IntlDateTimePatternGenerator_object_handlers.dtor_obj = IntlDateTimePatternGenerator_object_dtor; IntlDateTimePatternGenerator_object_handlers.free_obj = IntlDateTimePatternGenerator_object_free; if(!IntlDateTimePatternGenerator_ce) { zend_error(E_ERROR, "Failed to register IntlDateTimePatternGenerator class"); return FAILURE; } return SUCCESS; } /* }}} */ /* {{{ PHP_MSHUTDOWN_FUNCTION */ PHP_MSHUTDOWN_FUNCTION(intl_dtpg) { return SUCCESS; } /* }}} */ /* {{{ PHP_MINFO_FUNCTION */ PHP_MINFO_FUNCTION(intl_dtpg) { php_info_print_table_start(); php_info_print_table_header(2, "intl_dtpg support", "enabled"); php_info_print_table_header(2, "intl_dtpg version", INTL_DTPG_VERSION); php_info_print_table_end(); } /* }}} */ /* {{{ intl_dtpg_functions[] */ const zend_function_entry intl_dtpg_functions[] = { PHP_FE_END }; /* }}} */ /* {{{ intl_dtpg_module_entry */ zend_module_entry intl_dtpg_module_entry = { STANDARD_MODULE_HEADER, "intl_dtpg", intl_dtpg_functions, PHP_MINIT(intl_dtpg), PHP_MSHUTDOWN(intl_dtpg), NULL, NULL, PHP_MINFO(intl_dtpg), INTL_DTPG_VERSION, STANDARD_MODULE_PROPERTIES }; /* }}} */ #ifdef COMPILE_DL_INTL_DTPG #ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE() #endif ZEND_GET_MODULE(intl_dtpg) #endif 


First, we define handlers that will be executed in certain phases of the life of our PHP object.

IntlDateTimePatternGenerator_object_dtor - PHP object destructor. There is nothing to do, we call the standard API.
IntlDateTimePatternGenerator_object_free - freeing memory. An important phase, you need to do everything carefully so that there are no memory leaks. We zend_object our structure, call the destructor for zend_object , reset the status and destroy the C ++ class object.
IntlDateTimePatternGenerator_object_create - memory allocation for a new object. The code is peeped in the intl source code.
Next, we define the methods of our class.
PHP_METHOD (IntlDateTimePatternGenerator, __construct) - constructor, a new DateTimePatternGenerator object is created.
Here, in zend_parse_parameters , we get a string in the form of a zend_string object, while a large “S” is indicated in zend_parse_parameters . This is an innovation in PHP 7. But the old way of indicating a small “s” and getting a separate C-style line and its length also works, as we saw in the previous extension.
PHP_METHOD (IntlDateTimePatternGenerator, findBestPattern) - the class method from the ICU library is called here.
The following is a set of macros to define the parameters of the class methods. By the name of the parameters of these macros in the zend_API.h file, zend_API.h can understand what they mean.
The zend_function_entry structure specifies the class methods. The previously defined parameter sets and flags are passed to the PHP_ME macro. This structure is used when registering a class below.
PHP_MINIT_FUNCTION (intl_dtpg) - called when the extension is initialized. Here our class is registered and handlers are indicated to serve its phases of life.
PHP_MSHUTDOWN_FUNCTION (intl_dtpg) - when there is nothing to do at the time of shutdown, you can simply return SUCCESS , but you can also not specify in the zend_module_entry , specify NULL below.
PHP_MINFO_FUNCTION (intl_dtpg) - adds extension information to the phpinfo () output
Again zend_function_entry , but this time empty - we do not define any functions in this extension.
zend_module_entry - here we specify the methods for initialization and shutdown of our extension, the other two NULL are about request, we do nothing at the moment of initialization and shutdown of the request.

tests


A small test, make sure that the patterns are generated:

001.phpt
 --TEST-- Check for intl_dtpg presence --SKIPIF-- <?php if (!extension_loaded("intl_dtpg")) print "skip"; ?> --FILE-- <?php $dtpg = new IntlDateTimePatternGenerator('ru_RU'); $ruPattern = $dtpg->findBestPattern('MMMMd'); $dtpg = new IntlDateTimePatternGenerator('en_US'); $enPattern = $dtpg->findBestPattern('MMMMd'); echo $ruPattern . ';' . $enPattern; ?> --EXPECT-- d MMMM;MMMM d 


Build and test


 phpize && ./configure && make make test 


valgrind - we check extensions for memory leaks


Check our intl_dtpg extension for memory leaks. To do this, create a test ini file in the extension folder:

test-php.ini
 extension=modules/intl_dtpg.so 


and test php file:
test.php
 <?php $dtpg = new IntlDateTimePatternGenerator('ru_RU'); $ruPattern = $dtpg->findBestPattern('MMMMd'); $dtpg = new IntlDateTimePatternGenerator('en_US'); $enPattern = $dtpg->findBestPattern('MMMMd'); echo $ruPattern . ';' . $enPattern . "\n"; 


Checking:

 valgrind php -c test-php.ini test.php 

==30326== HEAP SUMMARY:
==30326== in use at exit: 478,022 bytes in 2,151 blocks
==30326== total heap usage: 22,863 allocs, 20,712 frees, 4,718,469 bytes allocated
==30326==
==30326== LEAK SUMMARY:
==30326== definitely lost: 0 bytes in 0 blocks
==30326== indirectly lost: 0 bytes in 0 blocks
==30326== possibly lost: 1,076 bytes in 14 blocks
==30326== still reachable: 476,946 bytes in 2,137 blocks
==30326== of which reachable via heuristic:
==30326== newarray : 30,416 bytes in 74 blocks
==30326== suppressed: 0 bytes in 0 blocks


Seems not bad.

In order to experiment, comment out the destruction of the object.

DateTimePatternGenerator in the IntlDateTimePatternGenerator_object_free IntlDateTimePatternGenerator_object_free

 /* {{{ IntlDateTimePatternGenerator_objects_free */ void IntlDateTimePatternGenerator_object_free(zend_object *object) { IntlDateTimePatternGenerator_object *dtpgo = php_intl_datetimepatterngenerator_fetch_object(object); zend_object_std_dtor(&dtpgo->zo); dtpgo->status = U_ZERO_ERROR; // if (dtpgo->dtpg) { // delete dtpgo->dtpg; // dtpgo->dtpg = nullptr; // } } /* }}} */ 

Checking:

 valgrind php -c test-php.ini test.php 

==411== HEAP SUMMARY:
==411== in use at exit: 770,710 bytes in 2,477 blocks
==411== total heap usage: 22,863 allocs, 20,386 frees, 4,718,469 bytes allocated
==411==
==411== LEAK SUMMARY:
==411== definitely lost: 5,232 bytes in 2 blocks
==411== indirectly lost: 287,456 bytes in 324 blocks
==411== possibly lost: 1,076 bytes in 14 blocks
==411== still reachable: 476,946 bytes in 2,137 blocks
==411== of which reachable via heuristic:
==411== newarray : 30,416 bytes in 74 blocks
==411== suppressed: 0 bytes in 0 blocks

The lines “definitely lost” and “indirectly lost” clearly indicate a leak.

Conclusion


I want to say that the documentation and naming API leave much to be desired. If you need to write something more complicated than “Hello, world”, you will have to learn the source code of PHP and the built-in extensions.

Source: https://habr.com/ru/post/335478/


All Articles