Skip to content

Commit e4f7df0

Browse files
committed
Improve example http-client.
1 parent a7e50d8 commit e4f7df0

File tree

12 files changed

+197
-71
lines changed

12 files changed

+197
-71
lines changed

examples/http-client/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ anyhow = "1.0.40"
1616
bytes = "1.0.1"
1717
indexmap = "1.6.2"
1818
phper = { version = "0.2.0-alpha.3", path = "../../phper" }
19-
reqwest = { version = "0.11.3", features = ["blocking"] }
19+
reqwest = { version = "0.11.3", features = ["blocking", "cookies"] }
2020
thiserror = "1.0.24"
2121

2222
[dev-dependencies]

examples/http-client/src/client.rs

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,104 @@
11
use crate::{
2-
errors::HttpClientError,
3-
response::{ReadiedResponse, RESPONSE_CLASS_NAME},
2+
errors::HttpClientError, replace_and_get, replace_and_set, request::REQUEST_BUILDER_CLASS_NAME,
3+
response::RESPONSE_CLASS_NAME,
44
};
5-
65
use phper::{
7-
classes::{DynamicClass, Visibility},
6+
classes::{ClassEntry, DynamicClass, Visibility},
87
functions::Argument,
98
objects::Object,
109
};
11-
use reqwest::blocking::{Client, ClientBuilder};
12-
use std::time::Duration;
10+
use reqwest::blocking::{Client, ClientBuilder, RequestBuilder};
11+
use std::{convert::TryInto, mem::swap, time::Duration};
1312

13+
const HTTP_CLIENT_BUILDER_CLASS_NAME: &'static str = "HttpClient\\HttpClientBuilder";
1414
const HTTP_CLIENT_CLASS_NAME: &'static str = "HttpClient\\HttpClient";
1515

16-
pub fn make_client_class() -> DynamicClass<Client> {
17-
let mut class = DynamicClass::new_with_constructor(HTTP_CLIENT_CLASS_NAME, || {
18-
let client = ClientBuilder::new()
19-
.timeout(Duration::from_secs(15))
20-
.build()?;
21-
Ok::<_, HttpClientError>(client)
22-
});
16+
pub fn make_client_builder_class() -> DynamicClass<ClientBuilder> {
17+
let mut class = DynamicClass::new_with_default(HTTP_CLIENT_BUILDER_CLASS_NAME);
2318

2419
class.add_method(
25-
"get",
20+
"timeout",
2621
Visibility::Public,
2722
|this, arguments| {
28-
let url = arguments[0].as_string()?;
29-
let client = this.as_state();
30-
let response = client.get(url).send()?;
23+
let ms = arguments[0].as_long()?;
24+
let state = this.as_mut_state();
25+
replace_and_set(state, ClientBuilder::new(), |builder| {
26+
builder.timeout(Duration::from_millis(ms as u64))
27+
});
28+
Ok::<_, HttpClientError>(())
29+
},
30+
vec![Argument::by_val("ms")],
31+
);
32+
33+
class.add_method(
34+
"cookie_store",
35+
Visibility::Public,
36+
|this, arguments| {
37+
let enable = arguments[0].as_bool()?;
38+
let state = this.as_mut_state();
39+
replace_and_set(state, ClientBuilder::new(), |builder| {
40+
builder.cookie_store(enable)
41+
});
42+
Ok::<_, HttpClientError>(())
43+
},
44+
vec![Argument::by_val("enable")],
45+
);
3146

32-
let readied_response = ReadiedResponse {
33-
status: response.status(),
34-
remote_addr: response.remote_addr(),
35-
headers: response.headers().clone(),
36-
body: response.bytes()?,
37-
};
47+
class.add_method(
48+
"build",
49+
Visibility::Public,
50+
|this, arguments| {
51+
let state = this.as_mut_state();
52+
let client = replace_and_get(state, ClientBuilder::new(), ClientBuilder::build)?;
53+
let mut object =
54+
ClassEntry::<Option<Client>>::from_globals(HTTP_CLIENT_CLASS_NAME)?.new_object();
55+
*object.as_mut_state() = Some(client);
56+
Ok::<_, HttpClientError>(object)
57+
},
58+
vec![],
59+
);
3860

39-
let mut response_object =
40-
Object::<Option<ReadiedResponse>>::new_by_class_name(RESPONSE_CLASS_NAME)?;
41-
*response_object.as_mut_state() = Some(readied_response);
61+
class
62+
}
4263

43-
Ok::<_, HttpClientError>(response_object)
64+
pub fn make_client_class() -> DynamicClass<Option<Client>> {
65+
let mut class = DynamicClass::new_with_none(HTTP_CLIENT_CLASS_NAME);
66+
67+
class.add_method(
68+
"__construct",
69+
Visibility::Private,
70+
|_: &mut Object<Option<Client>>, _| {},
71+
vec![],
72+
);
73+
74+
class.add_method(
75+
"get",
76+
Visibility::Public,
77+
|this, arguments| {
78+
let url = arguments[0].as_string()?;
79+
let client = this.as_state().as_ref().unwrap();
80+
let request_builder = client.get(url);
81+
let mut object =
82+
ClassEntry::<Option<RequestBuilder>>::from_globals(REQUEST_BUILDER_CLASS_NAME)?
83+
.new_object();
84+
*object.as_mut_state() = Some(request_builder);
85+
Ok::<_, HttpClientError>(object)
86+
},
87+
vec![Argument::by_val("url")],
88+
);
89+
90+
class.add_method(
91+
"post",
92+
Visibility::Public,
93+
|this, arguments| {
94+
let url = arguments[0].as_string()?;
95+
let client = this.as_state().as_ref().unwrap();
96+
let request_builder = client.post(url);
97+
let mut object =
98+
ClassEntry::<Option<RequestBuilder>>::from_globals(REQUEST_BUILDER_CLASS_NAME)?
99+
.new_object();
100+
*object.as_mut_state() = Some(request_builder);
101+
Ok::<_, HttpClientError>(object)
44102
},
45103
vec![Argument::by_val("url")],
46104
);

examples/http-client/src/errors.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,20 @@ pub enum HttpClientError {
1212

1313
#[error(transparent)]
1414
Reqwest(#[from] reqwest::Error),
15+
16+
#[error("should call '{method_name}()' before call 'body()'")]
17+
ResponseAfterRead { method_name: String },
18+
19+
#[error("should not call 'body()' multi time")]
20+
ResponseHadRead,
1521
}
1622

1723
impl Throwable for HttpClientError {
1824
fn class_entry(&self) -> &StatelessClassEntry {
19-
ClassEntry::from_globals(EXCEPTION_CLASS_NAME).unwrap()
25+
match self {
26+
HttpClientError::Phper(e) => e.class_entry(),
27+
_ => ClassEntry::from_globals(EXCEPTION_CLASS_NAME).unwrap(),
28+
}
2029
}
2130
}
2231

examples/http-client/src/lib.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use crate::{
2-
client::make_client_class, errors::make_exception_class, response::make_response_class,
2+
client::{make_client_builder_class, make_client_class},
3+
errors::make_exception_class,
4+
request::make_request_builder_class,
5+
response::make_response_class,
36
};
47
use phper::{modules::Module, php_get_module};
8+
use std::mem::{replace, swap};
59

610
pub mod client;
711
pub mod errors;
@@ -16,9 +20,20 @@ pub fn get_module() -> Module {
1620
env!("CARGO_PKG_AUTHORS"),
1721
);
1822

19-
module.add_class(make_client_class());
2023
module.add_class(make_exception_class());
24+
module.add_class(make_client_class());
25+
module.add_class(make_client_builder_class());
26+
module.add_class(make_request_builder_class());
2127
module.add_class(make_response_class());
2228

2329
module
2430
}
31+
32+
fn replace_and_set<T>(t: &mut T, mut init: T, f: impl FnOnce(T) -> T) {
33+
let x = f(replace(t, init));
34+
let _ = replace(t, x);
35+
}
36+
37+
fn replace_and_get<T, R>(t: &mut T, mut init: T, f: impl FnOnce(T) -> R) -> R {
38+
f(replace(t, init))
39+
}

examples/http-client/src/request.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
1-
use phper::classes::DynamicClass;
2-
use reqwest::blocking::{Request, RequestBuilder};
1+
use crate::{errors::HttpClientError, replace_and_get, response::RESPONSE_CLASS_NAME};
2+
use phper::{
3+
classes::{ClassEntry, DynamicClass, Visibility},
4+
objects::Object,
5+
};
6+
use reqwest::blocking::{Request, RequestBuilder, Response};
37

48
pub const REQUEST_BUILDER_CLASS_NAME: &'static str = "HttpClient\\RequestBuilder";
59

610
pub fn make_request_builder_class() -> DynamicClass<Option<RequestBuilder>> {
711
let mut class = DynamicClass::new_with_none(REQUEST_BUILDER_CLASS_NAME);
812

13+
class.add_method(
14+
"__construct",
15+
Visibility::Private,
16+
|_: &mut Object<Option<RequestBuilder>>, _| {},
17+
vec![],
18+
);
19+
20+
class.add_method(
21+
"send",
22+
Visibility::Public,
23+
|this, arguments| {
24+
let state = this.as_mut_state();
25+
let response = replace_and_get(state, None, |builder| builder.unwrap().send())?;
26+
let mut object =
27+
ClassEntry::<Option<Response>>::from_globals(RESPONSE_CLASS_NAME)?.new_object();
28+
*object.as_mut_state() = Some(response);
29+
Ok::<_, HttpClientError>(object)
30+
},
31+
vec![],
32+
);
33+
934
class
1035
}

examples/http-client/src/response.rs

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
1+
use crate::{errors::HttpClientError, replace_and_get};
12
use bytes::Bytes;
23
use indexmap::map::IndexMap;
34
use phper::{
45
classes::{DynamicClass, Visibility},
56
objects::Object,
67
};
7-
use reqwest::{header::HeaderMap, StatusCode};
8+
use reqwest::{blocking::Response, header::HeaderMap, StatusCode};
89
use std::{convert::Infallible, net::SocketAddr};
910

1011
pub const RESPONSE_CLASS_NAME: &'static str = "HttpClient\\Response";
1112

12-
pub struct ReadiedResponse {
13-
pub status: StatusCode,
14-
pub remote_addr: Option<SocketAddr>,
15-
pub headers: HeaderMap,
16-
pub body: Bytes,
17-
}
18-
19-
pub fn make_response_class() -> DynamicClass<Option<ReadiedResponse>> {
13+
pub fn make_response_class() -> DynamicClass<Option<Response>> {
2014
let mut class = DynamicClass::new_with_none(RESPONSE_CLASS_NAME);
2115

2216
class.add_method(
2317
"body",
2418
Visibility::Public,
25-
|this: &mut Object<Option<ReadiedResponse>>, _arguments| {
26-
let readied_response = this.as_state().as_ref().unwrap();
27-
let body: &[u8] = readied_response.body.as_ref();
28-
body.to_vec()
19+
|this: &mut Object<Option<Response>>, _arguments| {
20+
let response = this.as_mut_state();
21+
let body = replace_and_get(response, None, |response| {
22+
response
23+
.ok_or(HttpClientError::ResponseHadRead)
24+
.and_then(|response| response.bytes().map_err(Into::into))
25+
})?;
26+
Ok::<_, HttpClientError>((&body).to_vec())
2927
},
3028
vec![],
3129
);
@@ -34,8 +32,13 @@ pub fn make_response_class() -> DynamicClass<Option<ReadiedResponse>> {
3432
"status",
3533
Visibility::Public,
3634
|this, _arguments| {
37-
let readied_response = this.as_state().as_ref().unwrap();
38-
readied_response.status.as_u16() as i64
35+
let response =
36+
this.as_state()
37+
.as_ref()
38+
.ok_or_else(|| HttpClientError::ResponseAfterRead {
39+
method_name: "status".to_owned(),
40+
})?;
41+
Ok::<_, HttpClientError>(response.status().as_u16() as i64)
3942
},
4043
vec![],
4144
);
@@ -44,18 +47,23 @@ pub fn make_response_class() -> DynamicClass<Option<ReadiedResponse>> {
4447
"headers",
4548
Visibility::Public,
4649
|this, _arguments| {
47-
let readied_response = this.as_state().as_ref().unwrap();
50+
let response =
51+
this.as_state()
52+
.as_ref()
53+
.ok_or_else(|| HttpClientError::ResponseAfterRead {
54+
method_name: "headers".to_owned(),
55+
})?;
4856
let headers_map =
49-
readied_response
50-
.headers
57+
response
58+
.headers()
5159
.iter()
5260
.fold(IndexMap::new(), |mut acc, (key, value)| {
5361
acc.entry(key.as_str().to_owned())
5462
.or_insert(vec![])
5563
.push(value.as_bytes().to_owned());
5664
acc
5765
});
58-
headers_map
66+
Ok::<_, HttpClientError>(headers_map)
5967
},
6068
vec![],
6169
);
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
<?php
22

3+
use HttpClient\HttpClientBuilder;
34
use HttpClient\HttpClient;
45
use HttpClient\HttpClientException;
56

67
ini_set("display_errors", "On");
78
ini_set("display_startup_errors", "On");
89
error_reporting(E_ALL);
910

10-
$client = new HttpClient();
11+
$client_builder = new HttpClientBuilder();
12+
$client_builder->timeout(15000);
13+
$client_builder->cookie_store(true);
14+
$client = $client_builder->build();
1115

12-
$resp = $client->get("https://httpbin.org/ip");
16+
$request_builder = $client->get("https://httpbin.org/ip");
17+
$response = $request_builder->send();
1318
var_dump([
14-
"status" => $resp->status(),
15-
"headers" => $resp->headers(),
16-
"body" => $resp->body(),
19+
"status" => $response->status(),
20+
"headers" => $response->headers(),
21+
"body" => $response->body(),
1722
]);
1823

1924
try {
20-
$client->get("file:///");
25+
$client->get("file:///")->send();
2126
throw new AssertionError("no throw exception");
2227
} catch (HttpClientException $e) {
2328
}

phper-sys/php_wrapper.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ void phper_zval_obj(zval *z, zend_object *o) {
9494
ZVAL_OBJ(z, o);
9595
}
9696

97+
void phper_zval_obj_copy(zval *z, zend_object *o) {
98+
ZVAL_OBJ_COPY(z, o);
99+
}
100+
97101
#if PHP_VERSION_ID < 80000
98102
static zend_string *phper_zend_string_concat3(
99103
const char *str1, size_t str1_len,

phper-sys/php_wrapper.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ void phper_array_init(zval *arg);
3939
void *phper_zend_hash_str_find_ptr(const HashTable *ht, const char *str, size_t len);
4040

4141
void phper_zval_obj(zval *z, zend_object *o);
42+
void phper_zval_obj_copy(zval *z, zend_object *o);
4243

4344
zend_string *phper_get_function_or_method_name(const zend_function *func);
4445

0 commit comments

Comments
 (0)