diff --git a/Cargo.toml b/Cargo.toml index 85c701e1..b7253738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "phper-macros", "phper-sys", "phper-test", + "phper-doc", # internal "examples/complex", diff --git a/README.md b/README.md index 53fdb852..bc9781f2 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,16 @@ The framework that allows us to write PHP extensions using pure and safe Rust whenever possible. +## Document & Tutorial + +- Document: +- Tutorial: + ## Requirement ### Necessary -- **rust** 1.56 or later +- **rust** 1.65 or later - **libclang** 9.0 or later - **php** 7.0 or later @@ -43,83 +48,6 @@ The framework that allows us to write PHP extensions using pure and safe Rust wh - [x] disable - [ ] enable -## Usage - -1. Make sure `libclang` and `php` is installed. - - ```bash - # If you are using debian like linux system: - sudo apt install llvm-10-dev libclang-10-dev php-cli - ``` - -1. Create you cargo project, suppose your application is called myapp. - - ```bash - cargo new myapp - ``` - -1. Add the dependencies and metadata to you Cargo project. - - ```toml - [lib] - crate-type = ["cdylib"] - - [dependencies] - phper = "" - ``` - -1. Create the `build.rs` ( Adapting MacOS ). - - ```rust,no_run - fn main() { - #[cfg(target_os = "macos")] - { - println!("cargo:rustc-link-arg=-undefined"); - println!("cargo:rustc-link-arg=dynamic_lookup"); - } - } - ``` - -1. Write you owned extension logic in `lib.rs`. - - ```rust - use phper::{php_get_module, modules::Module}; - - #[php_get_module] - pub fn get_module() -> Module { - let mut module = Module::new( - env!("CARGO_CRATE_NAME"), - env!("CARGO_PKG_VERSION"), - env!("CARGO_PKG_AUTHORS"), - ); - - // ... - - module - } - ``` - -1. Build and install, if your php isn't installed globally, you should specify the path of `php-config`. - - ```bash - # Optional, specify if php isn't installed globally. - # export PHP_CONFIG= - - # Build libmyapp.so. - cargo build --release - - # Install to php extension path. - cp target/release/libmyapp.so `${PHP_CONFIG:=php-config} --extension-dir` - ``` - -1. Edit your `php.ini`, add the below line. - - ```ini - extension = myapp - ``` - -1. Enjoy. - ## Examples See [examples](https://github.com/phper-framework/phper/tree/master/examples). diff --git a/examples/http-client/Cargo.toml b/examples/http-client/Cargo.toml index b0b278e4..e415e0e0 100644 --- a/examples/http-client/Cargo.toml +++ b/examples/http-client/Cargo.toml @@ -21,9 +21,6 @@ license = { workspace = true } crate-type = ["lib", "cdylib"] [dependencies] -anyhow = "1.0.66" -bytes = "1.3.0" -indexmap = "1.9.2" phper = { version = "0.6.0", path = "../../phper" } reqwest = { version = "0.11.13", features = ["blocking", "cookies"] } thiserror = "1.0.37" diff --git a/examples/http-client/src/client.rs b/examples/http-client/src/client.rs index b4e51c5f..2638f930 100644 --- a/examples/http-client/src/client.rs +++ b/examples/http-client/src/client.rs @@ -8,57 +8,59 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. -use crate::{ - errors::HttpClientError, - request::REQUEST_BUILDER_CLASS_NAME, - utils::{replace_and_get, replace_and_set}, -}; +use crate::{errors::HttpClientError, request::REQUEST_BUILDER_CLASS_NAME}; use phper::{ alloc::ToRefOwned, classes::{ClassEntry, StatefulClass, Visibility}, functions::Argument, }; use reqwest::blocking::{Client, ClientBuilder}; -use std::time::Duration; +use std::{mem::take, time::Duration}; const HTTP_CLIENT_BUILDER_CLASS_NAME: &str = "HttpClient\\HttpClientBuilder"; const HTTP_CLIENT_CLASS_NAME: &str = "HttpClient\\HttpClient"; pub fn make_client_builder_class() -> StatefulClass { + // `new_with_default_state` means initialize the state of `ClientBuilder` as + // `Default::default`. let mut class = StatefulClass::new_with_default_state(HTTP_CLIENT_BUILDER_CLASS_NAME); + // Inner call the `ClientBuilder::timeout`. class.add_method( "timeout", Visibility::Public, |this, arguments| { let ms = arguments[0].expect_long()?; - let state: &mut ClientBuilder = this.as_mut_state(); - replace_and_set(state, |builder| { - builder.timeout(Duration::from_millis(ms as u64)) - }); + let state = this.as_mut_state(); + let builder: ClientBuilder = take(state); + *state = builder.timeout(Duration::from_millis(ms as u64)); Ok::<_, HttpClientError>(this.to_ref_owned()) }, vec![Argument::by_val("ms")], ); + // Inner call the `ClientBuilder::cookie_store`. class.add_method( "cookie_store", Visibility::Public, |this, arguments| { let enable = arguments[0].expect_bool()?; - let state: &mut ClientBuilder = this.as_mut_state(); - replace_and_set(state, |builder| builder.cookie_store(enable)); + let state = this.as_mut_state(); + let builder: ClientBuilder = take(state); + *state = builder.cookie_store(enable); Ok::<_, HttpClientError>(this.to_ref_owned()) }, vec![Argument::by_val("enable")], ); + // Inner call the `ClientBuilder::build`, and wrap the result `Client` in + // Object. class.add_method( "build", Visibility::Public, |this, _arguments| { - let state = this.as_mut_state(); - let client = replace_and_get(state, ClientBuilder::build)?; + let state = take(this.as_mut_state()); + let client = ClientBuilder::build(state)?; let mut object = ClassEntry::from_globals(HTTP_CLIENT_CLASS_NAME)?.init_object()?; unsafe { *object.as_mut_state() = Some(client); @@ -80,7 +82,7 @@ pub fn make_client_class() -> StatefulClass> { "get", Visibility::Public, |this, arguments| { - let url = arguments[0].as_z_str().unwrap().to_str().unwrap(); + let url = arguments[0].expect_z_str()?.to_str().unwrap(); let client = this.as_state().as_ref().unwrap(); let request_builder = client.get(url); let mut object = ClassEntry::from_globals(REQUEST_BUILDER_CLASS_NAME)?.init_object()?; @@ -96,7 +98,7 @@ pub fn make_client_class() -> StatefulClass> { "post", Visibility::Public, |this, arguments| { - let url = arguments[0].as_z_str().unwrap().to_str().unwrap(); + let url = arguments[0].expect_z_str()?.to_str().unwrap(); let client = this.as_state().as_ref().unwrap(); let request_builder = client.post(url); let mut object = ClassEntry::from_globals(REQUEST_BUILDER_CLASS_NAME)?.init_object()?; diff --git a/examples/http-client/src/errors.rs b/examples/http-client/src/errors.rs index 280cf633..10d2bc22 100644 --- a/examples/http-client/src/errors.rs +++ b/examples/http-client/src/errors.rs @@ -10,11 +10,15 @@ use phper::classes::{ClassEntry, StatefulClass}; +/// The exception class name of extension. const EXCEPTION_CLASS_NAME: &str = "HttpClient\\HttpClientException"; +/// The struct implemented `phper::Throwable` will throw php Exception +/// when return as `Err(e)` in extension functions. #[derive(Debug, thiserror::Error, phper::Throwable)] #[throwable_class(EXCEPTION_CLASS_NAME)] pub enum HttpClientError { + /// Generally, implement `From` for `phper::Error`. #[error(transparent)] #[throwable(transparent)] Phper(#[from] phper::Error), @@ -31,6 +35,7 @@ pub enum HttpClientError { pub fn make_exception_class() -> StatefulClass<()> { let mut exception_class = StatefulClass::new(EXCEPTION_CLASS_NAME); + // The `extends` is same as the PHP class `extends`. exception_class.extends("Exception"); exception_class } diff --git a/examples/http-client/src/lib.rs b/examples/http-client/src/lib.rs index 8400dc9d..db34a621 100644 --- a/examples/http-client/src/lib.rs +++ b/examples/http-client/src/lib.rs @@ -20,7 +20,6 @@ pub mod client; pub mod errors; pub mod request; pub mod response; -pub mod utils; #[php_get_module] pub fn get_module() -> Module { diff --git a/examples/http-client/src/request.rs b/examples/http-client/src/request.rs index 36f8c8da..47e2c05c 100644 --- a/examples/http-client/src/request.rs +++ b/examples/http-client/src/request.rs @@ -8,9 +8,10 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. -use crate::{errors::HttpClientError, response::RESPONSE_CLASS_NAME, utils::replace_and_get}; +use crate::{errors::HttpClientError, response::RESPONSE_CLASS_NAME}; use phper::classes::{ClassEntry, StatefulClass, Visibility}; use reqwest::blocking::RequestBuilder; +use std::mem::take; pub const REQUEST_BUILDER_CLASS_NAME: &str = "HttpClient\\RequestBuilder"; @@ -24,8 +25,8 @@ pub fn make_request_builder_class() -> StatefulClass> { "send", Visibility::Public, |this, _arguments| { - let state = this.as_mut_state(); - let response = replace_and_get(state, |builder| builder.unwrap().send())?; + let state = take(this.as_mut_state()); + let response = state.unwrap().send()?; let mut object = ClassEntry::from_globals(RESPONSE_CLASS_NAME)?.new_object([])?; unsafe { *object.as_mut_state() = Some(response); diff --git a/examples/http-client/src/response.rs b/examples/http-client/src/response.rs index 9e6650e1..06cea230 100644 --- a/examples/http-client/src/response.rs +++ b/examples/http-client/src/response.rs @@ -8,13 +8,14 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. -use crate::{errors::HttpClientError, utils::replace_and_get}; +use crate::errors::HttpClientError; use phper::{ arrays::{InsertKey, ZArray}, classes::{StatefulClass, Visibility}, values::ZVal, }; use reqwest::blocking::Response; +use std::mem::take; pub const RESPONSE_CLASS_NAME: &str = "HttpClient\\Response"; @@ -25,12 +26,10 @@ pub fn make_response_class() -> StatefulClass> { "body", Visibility::Public, |this, _arguments| { - let response = this.as_mut_state(); - let body = replace_and_get(response, |response| { - response - .ok_or(HttpClientError::ResponseHadRead) - .and_then(|response| response.bytes().map_err(Into::into)) - })?; + let response = take(this.as_mut_state()); + let body = response + .ok_or(HttpClientError::ResponseHadRead) + .and_then(|response| response.bytes().map_err(Into::into))?; Ok::<_, HttpClientError>(body.to_vec()) }, vec![], diff --git a/examples/http-client/src/utils.rs b/examples/http-client/src/utils.rs deleted file mode 100644 index 0f10170e..00000000 --- a/examples/http-client/src/utils.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2022 PHPER Framework Team -// PHPER is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan -// PSL v2. You may obtain a copy of Mulan PSL v2 at: -// http://license.coscl.org.cn/MulanPSL2 -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY -// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -use std::mem::{replace, take}; - -pub fn replace_and_set(t: &mut T, f: impl FnOnce(T) -> T) { - let x = f(take(t)); - let _ = replace(t, x); -} - -pub fn replace_and_get(t: &mut T, f: impl FnOnce(T) -> R) -> R { - f(take(t)) -} diff --git a/phper-alloc/Cargo.toml b/phper-alloc/Cargo.toml index 3ba59e56..bd068c0e 100644 --- a/phper-alloc/Cargo.toml +++ b/phper-alloc/Cargo.toml @@ -10,14 +10,14 @@ [package] name = "phper-alloc" +description = "Alloc related items for phper crate." +keywords = ["php", "alloc"] version = { workspace = true } authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } -description = "Alloc related items for phper crate." repository = { workspace = true } license = { workspace = true } -keywords = ["php", "alloc"] [dependencies] phper-sys = { version = "0.6.0", path = "../phper-sys" } diff --git a/phper-build/Cargo.toml b/phper-build/Cargo.toml index 9cc80103..c8c2e078 100644 --- a/phper-build/Cargo.toml +++ b/phper-build/Cargo.toml @@ -10,14 +10,14 @@ [package] name = "phper-build" +description = "Generates stubs for project using phper." +keywords = ["php", "binding"] version = { workspace = true } authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } -description = "Generates stubs for project using phper." repository = { workspace = true } license = { workspace = true } -keywords = ["php", "binding"] [dependencies] phper-sys = { version = "0.6.0", path = "../phper-sys" } diff --git a/phper-doc/Cargo.toml b/phper-doc/Cargo.toml new file mode 100644 index 00000000..eadee258 --- /dev/null +++ b/phper-doc/Cargo.toml @@ -0,0 +1,25 @@ +# Copyright (c) 2022 PHPER Framework Team +# PHPER is licensed under Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan +# PSL v2. You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +# KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +# See the Mulan PSL v2 for more details. + +[package] +name = "phper-doc" +description = "The documentation of phper." +keywords = ["php", "documentation"] +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dev-dependencies] +phper = { version = "0.6.0", path = "../phper" } +thiserror = "1.0.37" +reqwest = { version = "0.11.13", features = ["blocking", "cookies"] } diff --git a/phper-doc/LICENSE b/phper-doc/LICENSE new file mode 120000 index 00000000..ea5b6064 --- /dev/null +++ b/phper-doc/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/phper-doc/README.md b/phper-doc/README.md new file mode 100644 index 00000000..c7d03259 --- /dev/null +++ b/phper-doc/README.md @@ -0,0 +1,9 @@ +# PHPER documentation + +This is the documentation of [phper](https://crates.io/crates/phper). + +**There is nothing here other than documentation, so you don't have to import this crate as a dependency.** + +## License + +[MulanPSL-2.0](https://github.com/phper-framework/phper/blob/master/LICENSE). diff --git a/phper-doc/doc/_01_introduction/index.md b/phper-doc/doc/_01_introduction/index.md new file mode 100644 index 00000000..6b4a4a92 --- /dev/null +++ b/phper-doc/doc/_01_introduction/index.md @@ -0,0 +1,25 @@ +# Introduction + +`PHPER` is the framework that allows us to write PHP extensions using pure and safe Rust whenever possible. + +`PHPER` means `PHP Enjoy Rust`. + +## Rust ❤️ PHP + +The crates are not only the PHP binding for Rust, but also the framework for writing PHP extension. + +## Purpose + +I used to use C language to write PHP extensions. At that time, C/C++ are the only way to write PHP extensions. + +But I found the problem is that using C language can easily cause memory problems, which is very troublesome when debugging PHP extensions. + +Moreover, third-party libraries in C language are not easy to use, and version compatibility problems are often encountered in dynamic linking, which is inconvenient to use. + +Later, Rust appeared, and I started to use C to call Rust's FFI to develop PHP extensions. The experience is better than only use C language. + +However, it is not convenient for Rust to generate C ABI and then call C, so I got the idea of using pure Rust to write PHP extensions. + +So I started to build the framework of phper. + +The other goal is to enable PHP to benefit from the Rust ecosystem. diff --git a/phper-doc/doc/_02_quick_start/_01_write_your_first_extension/index.md b/phper-doc/doc/_02_quick_start/_01_write_your_first_extension/index.md new file mode 100644 index 00000000..31787187 --- /dev/null +++ b/phper-doc/doc/_02_quick_start/_01_write_your_first_extension/index.md @@ -0,0 +1,110 @@ +# Write your first extension + +Here we will write the `hello world` extension, which has a function, receive the person name and echo hello to the person. + +Full example is . + +## Steps + +1. Make sure `libclang` is installed (required by [bindgen](https://rust-lang.github.io/rust-bindgen/requirements.html)). + + `phper` require libclang *9.0+*. + + ```shell + # If you are using debian like linux system: + sudo apt install llvm-10-dev libclang-10-dev + ``` + +1. Create the cargo project, with the extension name. + + ```shell + cargo new --lib hello + + cd hello + ``` + +1. Add the metadata to the `Cargo.toml` to build the `.so` file. + + ```toml + ## Cargo.toml + + [lib] + crate-type = ["cdylib"] + ``` + + Run the command to add `phper` dependency. + + ```shell + cargo add phper + ``` + +1. Create the `build.rs` ( Adapting MacOS ). + + ```rust,no_run + fn main() { + #[cfg(target_os = "macos")] + { + println!("cargo:rustc-link-arg=-undefined"); + println!("cargo:rustc-link-arg=dynamic_lookup"); + } + } + ``` + +1. Write these code in `src/lib.rs`. + + ```rust + use phper::{echo, functions::Argument, modules::Module, php_get_module, values::ZVal}; + + /// The php function, receive arguments with type `ZVal`. + fn say_hello(arguments: &mut [ZVal]) -> phper::Result<()> { + // Get the first argument, expect the type `ZStr`, and convert to Rust utf-8 + // str. + let name = arguments[0].expect_z_str()?.to_str()?; + + // Macro which do php internal `echo`. + echo!("Hello, {}!\n", name); + + Ok(()) + } + + /// This is the entry of php extension, the attribute macro `php_get_module` + /// will generate the `extern "C" fn`. + #[php_get_module] + pub fn get_module() -> Module { + // New `Module` with extension info. + let mut module = Module::new( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Register function `say_hello`, with one argument `name`. + module.add_function("say_hello", say_hello, vec![Argument::by_val("name")]); + + module + } + ``` + +1. Build, if your php isn't installed globally, you should specify the path of `php-config`. + + ```bash + ## Optional, specify if php isn't installed globally, + ## this environment is used by `phper-sys`. + ## + ## export PHP_CONFIG= + + ## Build libhello.so. + cargo build + ``` + +1. Run the php command with the extension. + + ```shell + php -d "extension=target/debug/libhello.so" -r "say_hello('Bob');" + ``` + + Then you can get the output: + + ```text + Hello, Bob! + ``` diff --git a/phper-doc/doc/_02_quick_start/_02_write_a_simple_http_client/index.md b/phper-doc/doc/_02_quick_start/_02_write_a_simple_http_client/index.md new file mode 100644 index 00000000..5dcffc15 --- /dev/null +++ b/phper-doc/doc/_02_quick_start/_02_write_a_simple_http_client/index.md @@ -0,0 +1,300 @@ +# Write a simple http client + +Here we will use Rust crate [reqwest](https://crates.io/crates/reqwest) to write a simple http client, +like curl, but object-oriented. + +Full example is . + +Imagine that our PHP API should look like this: + +```php +timeout(15000) + ->cookie_store(true) + ->build(); + +$response = $client->get("https://httpbin.org/ip")->send(); +var_dump([ + "status" => $response->status(), + "headers" => $response->headers(), + "body" => $response->body(), +]); +``` + +Here, the namespace of API is `HttpClient`. + +And there are three class: + +- `HttpClientBuilder` is the builder of `HttpClient`. +- `HttpClient` will send a http request and generate a http response. +- `HttpClientException` will be throw when http request failed. + +## Steps + +Before writing the code, we first prepare the dependency and startup code. + +1. Make sure `libclang` is installed (required by [bindgen](https://rust-lang.github.io/rust-bindgen/requirements.html)). + + `phper` require libclang *9.0+*. + + ```shell + # If you are using debian like linux system: + sudo apt install llvm-10-dev libclang-10-dev + ``` + +1. Create the cargo project, with the extension name. + + ```shell + cargo new --lib http-client + + cd http-client + ``` + +1. Add the metadata to the `Cargo.toml` to build the `.so` file. + + ```toml + ## Cargo.toml + + [lib] + crate-type = ["cdylib"] + ``` + + Run the command to add dependencies. + + ```shell + cargo add phper + cargo add reqwest --features blocking --features cookies + cargo add thiserror + ``` + +1. Create the `build.rs` ( Adapting MacOS ). + + ```rust,no_run + fn main() { + #[cfg(target_os = "macos")] + { + println!("cargo:rustc-link-arg=-undefined"); + println!("cargo:rustc-link-arg=dynamic_lookup"); + } + } + ``` + +Now let's begin to finish the logic. + +1. First, we create `src/errors.rs` to make the `HttpClientException` class: + + ```rust + /*** src/errors.rs ***/ + + use phper::classes::{ClassEntry, StatefulClass}; + + /// The exception class name of extension. + const EXCEPTION_CLASS_NAME: &str = "HttpClient\\HttpClientException"; + + /// The struct implemented `phper::Throwable` will throw php Exception + /// when return as `Err(e)` in extension functions. + #[derive(Debug, thiserror::Error, phper::Throwable)] + #[throwable_class(EXCEPTION_CLASS_NAME)] + pub enum HttpClientError { + /// Generally, implement `From` for `phper::Error`. + #[error(transparent)] + #[throwable(transparent)] + Phper(#[from] phper::Error), + + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + + #[error("should call '{method_name}()' before call 'body()'")] + ResponseAfterRead { method_name: String }, + + #[error("should not call 'body()' multi time")] + ResponseHadRead, + } + + pub fn make_exception_class() -> StatefulClass<()> { + let mut exception_class = StatefulClass::new(EXCEPTION_CLASS_NAME); + // The `extends` is same as the PHP class `extends`. + exception_class.extends("Exception"); + exception_class + } + ``` + + > The `make_*_class` functions is for registering class in `src/lib.rs` later. + + > The `StatefulClass` represents the class entry hold the state as generic type, + > so you can wrap the Rust struct as state in PHP class, which is the common usage + > of class in php extensions (if using C/C++ to develop PHP extension, the PHP class + > commonly wrap the C/C++ pointer). + + > But here the `HttpClientException` hasn't state required, so the class in + > `StatefulClass<()>`. + +1. Then, create the `HttpClientBuilder` class in `src/client.rs`. + + ```rust + /*** src/errors.rs ***/ + + use phper::classes::{ClassEntry, StatefulClass}; + + /// The exception class name of extension. + const EXCEPTION_CLASS_NAME: &str = "HttpClient\\HttpClientException"; + + /// The struct implemented `phper::Throwable` will throw php Exception + /// when return as `Err(e)` in extension functions. + #[derive(Debug, thiserror::Error, phper::Throwable)] + #[throwable_class(EXCEPTION_CLASS_NAME)] + pub enum HttpClientError { + /// Generally, implement `From` for `phper::Error`. + #[error(transparent)] + #[throwable(transparent)] + Phper(#[from] phper::Error), + + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + + #[error("should call '{method_name}()' before call 'body()'")] + ResponseAfterRead { method_name: String }, + + #[error("should not call 'body()' multi time")] + ResponseHadRead, + } + + /*** src/client.rs ***/ + + use phper::{ + alloc::ToRefOwned, + classes::Visibility, + functions::Argument, + }; + use reqwest::blocking::{Client, ClientBuilder}; + use std::{mem::take, time::Duration}; + + const HTTP_CLIENT_BUILDER_CLASS_NAME: &str = "HttpClient\\HttpClientBuilder"; + const HTTP_CLIENT_CLASS_NAME: &str = "HttpClient\\HttpClient"; + + pub fn make_client_builder_class() -> StatefulClass { + // `new_with_default_state` means initialize the state of `ClientBuilder` as + // `Default::default`. + let mut class = StatefulClass::new_with_default_state(HTTP_CLIENT_BUILDER_CLASS_NAME); + + // Inner call the `ClientBuilder::timeout`. + class.add_method( + "timeout", + Visibility::Public, + |this, arguments| { + let ms = arguments[0].expect_long()?; + let state = this.as_mut_state(); + let builder: ClientBuilder = take(state); + *state = builder.timeout(Duration::from_millis(ms as u64)); + Ok::<_, HttpClientError>(this.to_ref_owned()) + }, + vec![Argument::by_val("ms")], + ); + + // Inner call the `ClientBuilder::cookie_store`. + class.add_method( + "cookie_store", + Visibility::Public, + |this, arguments| { + let enable = arguments[0].expect_bool()?; + let state = this.as_mut_state(); + let builder: ClientBuilder = take(state); + *state = builder.cookie_store(enable); + Ok::<_, HttpClientError>(this.to_ref_owned()) + }, + vec![Argument::by_val("enable")], + ); + + // Inner call the `ClientBuilder::build`, and wrap the result `Client` in Object. + class.add_method( + "build", + Visibility::Public, + |this, _arguments| { + let state = take(this.as_mut_state()); + let client = ClientBuilder::build(state)?; + let mut object = ClassEntry::from_globals(HTTP_CLIENT_CLASS_NAME)? + .init_object()?; + unsafe { + *object.as_mut_state() = Some(client); + } + Ok::<_, HttpClientError>(object) + }, + vec![], + ); + + class + } + ``` + +1. Follow this method to complete `HttpClient`, `RequestBuilder` and `Response`, see full example for details. + +1. Register all classes in `src/lib.rs`. + +1. All codes are finished, so we can build the extension `.so`, and run the + php script of the beginning of the tutorial with the extension. + + ```shell + cargo build + + php -d "extension=target/debug/libhttp_client.so" http-client.php + ``` + + Here is the result I got: + + ```text + array(3) { + ["status"]=> + int(200) + ["headers"]=> + array(7) { + ["date"]=> + array(1) { + [0]=> + string(29) "Sat, 03 Dec 2022 09:15:11 GMT" + } + ["content-type"]=> + array(1) { + [0]=> + string(16) "application/json" + } + ["content-length"]=> + array(1) { + [0]=> + string(2) "33" + } + ["connection"]=> + array(1) { + [0]=> + string(10) "keep-alive" + } + ["server"]=> + array(1) { + [0]=> + string(15) "gunicorn/19.9.0" + } + ["access-control-allow-origin"]=> + array(1) { + [0]=> + string(1) "*" + } + ["access-control-allow-credentials"]=> + array(1) { + [0]=> + string(4) "true" + } + } + ["body"]=> + string(33) "{ + "origin": "223.104.76.175" + } + " + } + ``` diff --git a/phper-doc/doc/_02_quick_start/index.md b/phper-doc/doc/_02_quick_start/index.md new file mode 100644 index 00000000..7a8b009e --- /dev/null +++ b/phper-doc/doc/_02_quick_start/index.md @@ -0,0 +1,8 @@ +# Quick start + +At the beginning, you need to: + +1. Be familiar with the Rust language. +2. Know the general process of PHP extension development. + +Here the tutorial will not explain the installation methods of Rust and PHP. diff --git a/phper-doc/src/lib.rs b/phper-doc/src/lib.rs new file mode 100644 index 00000000..818acd2f --- /dev/null +++ b/phper-doc/src/lib.rs @@ -0,0 +1,81 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +#![warn(rust_2018_idioms, clippy::dbg_macro)] +#![doc = include_str!("../README.md")] + +#[doc = include_str!("../doc/_01_introduction/index.md")] +pub mod _01_introduction {} + +#[doc = include_str!("../doc/_02_quick_start/index.md")] +pub mod _02_quick_start { + + #[doc = include_str!("../doc/_02_quick_start/_01_write_your_first_extension/index.md")] + pub mod _01_write_your_first_extension {} + + #[doc = include_str!("../doc/_02_quick_start/_02_write_a_simple_http_client/index.md")] + pub mod _02_write_a_simple_http_client {} +} + +/// TODO +pub mod _03_integrate_with_pecl {} + +/// TODO +pub mod _04_zval {} + +/// TODO +pub mod _05_internal_types { + + /// TODO + pub mod _01_z_str {} + + /// TODO + pub mod _02_z_arr {} + + /// TODO + pub mod _03_z_arr {} +} + +/// TODO +pub mod _06_class_and_object {} + +/// TODO +pub mod _07_module { + + /// TODO + pub mod _01_hooks {} + + /// TODO + pub mod _02_register_functions {} + + /// TODO + pub mod _03_register_constants {} + + /// TODO + pub mod _04_register_ini_settings {} + + /// TODO + pub mod _05_extension_information {} +} + +/// TODO +pub mod _08_allocation {} + +/// TODO +pub mod _09_handle_exception {} + +/// TODO +pub mod _10_build_script {} + +/// TODO +pub mod _11_integration_tests {} + +/// TODO +pub mod _12_macros {} diff --git a/phper-macros/Cargo.toml b/phper-macros/Cargo.toml index 446416bd..afaa52bc 100644 --- a/phper-macros/Cargo.toml +++ b/phper-macros/Cargo.toml @@ -10,14 +10,14 @@ [package] name = "phper-macros" +description = "The proc-macros for phper crate." +keywords = ["php", "proc-macro"] version = { workspace = true } authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } -description = "The proc-macros for phper crate." repository = { workspace = true } license = { workspace = true } -keywords = ["php", "proc-macro"] [lib] proc-macro = true diff --git a/phper-sys/Cargo.toml b/phper-sys/Cargo.toml index 6d8e8a5b..da009e2a 100644 --- a/phper-sys/Cargo.toml +++ b/phper-sys/Cargo.toml @@ -10,14 +10,14 @@ [package] name = "phper-sys" +description = "Low level PHP binding for Rust." +keywords = ["php", "binding"] version = { workspace = true } authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } -description = "Low level PHP binding for Rust." repository = { workspace = true } license = { workspace = true } -keywords = ["php", "binding"] [build-dependencies] bindgen = "0.63.0" diff --git a/phper-test/Cargo.toml b/phper-test/Cargo.toml index bce6adb9..7ce04ec4 100644 --- a/phper-test/Cargo.toml +++ b/phper-test/Cargo.toml @@ -10,14 +10,14 @@ [package] name = "phper-test" +description = "PHPer testing utilities." +keywords = ["php", "binding"] version = { workspace = true } authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } -description = "PHPer testing utilities." repository = { workspace = true } license = { workspace = true } -keywords = ["php", "binding"] [features] fpm = ["fastcgi-client", "tokio/full"] diff --git a/phper/Cargo.toml b/phper/Cargo.toml index df6eb16b..85acdcc2 100644 --- a/phper/Cargo.toml +++ b/phper/Cargo.toml @@ -10,16 +10,16 @@ [package] name = "phper" +description = "The framework that allows us to write PHP extensions using pure and safe Rust whenever possible." +documentation = "https://docs.rs/phper" +readme = "README.md" +keywords = ["php", "binding", "extension", "module"] version = { workspace = true } authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } -description = "The framework that allows us to write PHP extensions using pure and safe Rust whenever possible." repository = { workspace = true } -documentation = "https://docs.rs/phper" license = { workspace = true } -readme = "README.md" -keywords = ["php", "binding", "extension", "module"] [dependencies] anyhow = "1.0.66"