Add Memcache plugin. (#93)

diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 4ca62e4..bcfbcad 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -119,7 +119,7 @@
             bcmath, calendar, ctype, dom, exif, gettext, iconv, intl, json, mbstring,
             mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline, redis,
             memcached, swoole-${{ matrix.flag.swoole_version }}, xml, xmlreader, xmlwriter,
-            yaml, zip, mongodb
+            yaml, zip, mongodb, memcache
 
       - name: Setup php-fpm for Linux
         if: matrix.os == 'ubuntu-20.04'
diff --git a/Cargo.lock b/Cargo.lock
index eedb817..59476f4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1094,6 +1094,15 @@
 checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
 
 [[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
 name = "matches"
 version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1719,8 +1728,17 @@
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-automata",
- "regex-syntax",
+ "regex-automata 0.3.4",
+ "regex-syntax 0.7.4",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
 ]
 
 [[package]]
@@ -1731,11 +1749,17 @@
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax",
+ "regex-syntax 0.7.4",
 ]
 
 [[package]]
 name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
 version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
@@ -2540,10 +2564,14 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
 dependencies = [
+ "matchers",
  "nu-ansi-term",
+ "once_cell",
+ "regex",
  "sharded-slab",
  "smallvec",
  "thread_local",
+ "tracing",
  "tracing-core",
  "tracing-log",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index b190ec0..51c23e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -56,7 +56,7 @@
 tokio-stream = "0.1.14"
 tonic = { version = "0.8.3", features = ["tls"] }
 tracing = { version = "0.1.37", features = ["attributes"] }
-tracing-subscriber = "0.3.17"
+tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
 url = "2.4.0"
 
 [dev-dependencies]
diff --git a/README.md b/README.md
index 651bb8d..a256adb 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@
   * [x] [predis](https://github.com/predis/predis)
   * [x] [php-amqplib](https://github.com/php-amqplib/php-amqplib) for Message Queuing Producer
   * [x] [MongoDB](https://www.php.net/manual/en/set.mongodb.php)
+  * [x] [Memcache](https://www.php.net/manual/en/book.memcache.php)
 
 * Swoole Ecosystem
 
diff --git a/dist-material/LICENSE b/dist-material/LICENSE
index 93f09fe..0631fc3 100644
--- a/dist-material/LICENSE
+++ b/dist-material/LICENSE
@@ -392,6 +392,7 @@
     https://crates.io/crates/rand_core/0.6.4 0.6.4 Apache-2.0 OR MIT
     https://crates.io/crates/regex/1.9.1 1.9.1 Apache-2.0 OR MIT
     https://crates.io/crates/regex-automata/0.3.4 0.3.4 Apache-2.0 OR MIT
+    https://crates.io/crates/regex-syntax/0.6.29 0.6.29 Apache-2.0 OR MIT
     https://crates.io/crates/regex-syntax/0.7.4 0.7.4 Apache-2.0 OR MIT
     https://crates.io/crates/reqwest/0.11.18 0.11.18 Apache-2.0 OR MIT
     https://crates.io/crates/resolv-conf/0.7.0 0.7.0 Apache-2.0 OR MIT
@@ -530,6 +531,7 @@
     https://crates.io/crates/http-body/0.4.5 0.4.5 MIT
     https://crates.io/crates/hyper/0.14.27 0.14.27 MIT
     https://crates.io/crates/is-terminal/0.4.9 0.4.9 MIT
+    https://crates.io/crates/matchers/0.1.0 0.1.0 MIT
     https://crates.io/crates/matches/0.1.10 0.1.10 MIT
     https://crates.io/crates/mio/0.8.8 0.8.8 MIT
     https://crates.io/crates/nom/7.1.3 7.1.3 MIT
@@ -592,6 +594,7 @@
     https://crates.io/crates/globset/0.4.12 0.4.12 MIT OR Unlicense
     https://crates.io/crates/ignore/0.4.20 0.4.20 MIT OR Unlicense
     https://crates.io/crates/memchr/2.5.0 2.5.0 MIT OR Unlicense
+    https://crates.io/crates/regex-automata/0.1.10 0.1.10 MIT OR Unlicense
     https://crates.io/crates/same-file/1.0.6 1.0.6 MIT OR Unlicense
     https://crates.io/crates/walkdir/2.3.3 2.3.3 MIT OR Unlicense
     https://crates.io/crates/winapi-util/0.1.5 0.1.5 MIT OR Unlicense
diff --git a/dist-material/licenses/LICENSE-matchers.txt b/dist-material/licenses/LICENSE-matchers.txt
new file mode 100644
index 0000000..5858b17
--- /dev/null
+++ b/dist-material/licenses/LICENSE-matchers.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2019 Eliza Weisman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/dist-material/licenses/LICENSE-regex-automata.txt b/dist-material/licenses/LICENSE-regex-automata.txt
new file mode 100644
index 0000000..39d4bdb
--- /dev/null
+++ b/dist-material/licenses/LICENSE-regex-automata.txt
@@ -0,0 +1,25 @@
+Copyright (c) 2014 The Rust Project Developers
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/docs/en/setup/service-agent/php-agent/Supported-list.md b/docs/en/setup/service-agent/php-agent/Supported-list.md
index 9ad8476..c548229 100644
--- a/docs/en/setup/service-agent/php-agent/Supported-list.md
+++ b/docs/en/setup/service-agent/php-agent/Supported-list.md
@@ -15,6 +15,7 @@
 * [Memcached](https://www.php.net/manual/en/book.memcached.php)
 * [phpredis](https://github.com/phpredis/phpredis)
 * [MongoDB](https://www.php.net/manual/en/set.mongodb.php)
+* [Memcache](https://www.php.net/manual/en/book.memcache.php)
 
 ## Supported PHP library
 
diff --git a/src/module.rs b/src/module.rs
index 37b82b5..93f0cc6 100644
--- a/src/module.rs
+++ b/src/module.rs
@@ -36,7 +36,7 @@
     time::SystemTime,
 };
 use tracing::{debug, error, info, metadata::LevelFilter};
-use tracing_subscriber::FmtSubscriber;
+use tracing_subscriber::{EnvFilter, FmtSubscriber};
 
 static IS_ENABLE: Lazy<bool> = Lazy::new(|| {
     if !ini_get::<bool>(SKYWALKING_AGENT_ENABLE) {
@@ -247,8 +247,10 @@
 
     let file = open_options.open(path)?;
 
+    let filter = EnvFilter::new(format!("info,skywalking_agent={}", log_level));
+
     let subscriber = FmtSubscriber::builder()
-        .with_max_level(log_level)
+        .with_env_filter(filter)
         .with_ansi(false)
         .with_writer(file)
         .finish();
diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs
index a9264de..dbbcdd6 100644
--- a/src/plugin/mod.rs
+++ b/src/plugin/mod.rs
@@ -15,6 +15,7 @@
 
 mod plugin_amqplib;
 mod plugin_curl;
+mod plugin_memcache;
 mod plugin_memcached;
 mod plugin_mongodb;
 mod plugin_mysqli;
@@ -22,6 +23,7 @@
 mod plugin_predis;
 mod plugin_redis;
 mod plugin_swoole;
+mod style;
 
 use crate::execute::{AfterExecuteHook, BeforeExecuteHook};
 use once_cell::sync::Lazy;
@@ -43,6 +45,7 @@
         Box::<plugin_redis::RedisPlugin>::default(),
         Box::<plugin_amqplib::AmqplibPlugin>::default(),
         Box::<plugin_mongodb::MongodbPlugin>::default(),
+        Box::<plugin_memcache::MemcachePlugin>::default(),
     ]
 });
 
diff --git a/src/plugin/plugin_memcache.rs b/src/plugin/plugin_memcache.rs
new file mode 100644
index 0000000..84ab071
--- /dev/null
+++ b/src/plugin/plugin_memcache.rs
@@ -0,0 +1,319 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use super::{log_exception, style::ApiStyle, Plugin};
+use crate::{
+    component::COMPONENT_PHP_MEMCACHED_ID,
+    context::RequestContext,
+    execute::{AfterExecuteHook, BeforeExecuteHook, Noop},
+    tag::{CacheOp, TAG_CACHE_CMD, TAG_CACHE_KEY, TAG_CACHE_OP, TAG_CACHE_TYPE},
+};
+use dashmap::DashMap;
+use once_cell::sync::Lazy;
+use phper::{
+    arrays::IterKey,
+    objects::ZObj,
+    values::{ExecuteData, ZVal},
+};
+use skywalking::{
+    proto::v3::SpanLayer,
+    trace::span::{HandleSpanObject, Span},
+};
+use std::{any::Any, collections::HashMap};
+use tracing::{debug, error, instrument, warn};
+
+static PEER_MAP: Lazy<DashMap<u32, String>> = Lazy::new(Default::default);
+
+/// The method parameters is empty.
+static MEMCACHE_EMPTY_METHOD_MAPPING: Lazy<HashMap<&str, TagInfo<'static>>> =
+    Lazy::new(|| [("flush", TagInfo::new(None, None))].into_iter().collect());
+
+/// The method first parameter is key.
+static MEMCACHE_KEY_METHOD_MAPPING: Lazy<HashMap<&str, TagInfo<'static>>> = Lazy::new(|| {
+    [
+        ("set", TagInfo::new(Some("set"), Some(CacheOp::Write))),
+        ("add", TagInfo::new(Some("add"), Some(CacheOp::Write))),
+        (
+            "replace",
+            TagInfo::new(Some("replace"), Some(CacheOp::Write)),
+        ),
+        ("get", TagInfo::new(Some("get"), Some(CacheOp::Read))),
+        ("delete", TagInfo::new(Some("delete"), Some(CacheOp::Write))),
+        (
+            "increment",
+            TagInfo::new(Some("increment"), Some(CacheOp::Write)),
+        ),
+        (
+            "decrement",
+            TagInfo::new(Some("decrement"), Some(CacheOp::Write)),
+        ),
+    ]
+    .into_iter()
+    .collect()
+});
+
+struct TagInfo<'a> {
+    cmd: Option<&'a str>,
+    op: Option<CacheOp>,
+}
+
+impl<'a> TagInfo<'a> {
+    #[inline]
+    fn new(cmd: Option<&'a str>, op: Option<CacheOp>) -> Self {
+        Self { cmd, op }
+    }
+}
+
+#[derive(Default, Clone)]
+pub struct MemcachePlugin;
+
+impl Plugin for MemcachePlugin {
+    fn class_names(&self) -> Option<&'static [&'static str]> {
+        Some(&["Memcache", "MemcachePool"])
+    }
+
+    fn function_name_prefix(&self) -> Option<&'static str> {
+        Some("memcache_")
+    }
+
+    fn hook(
+        &self, class_name: Option<&str>, function_name: &str,
+    ) -> Option<(
+        Box<crate::execute::BeforeExecuteHook>,
+        Box<crate::execute::AfterExecuteHook>,
+    )> {
+        let lowercase_function_name = function_name.to_ascii_lowercase();
+        let function_name = function_name.to_owned();
+
+        match (class_name, &*lowercase_function_name) {
+            (Some("Memcache" | "MemcachePool"), "connect" | "addserver" | "close") => {
+                Some(self.hook_memcache_server(
+                    class_name.map(ToOwned::to_owned),
+                    function_name,
+                    ApiStyle::OO,
+                ))
+            }
+            (None, "memcache_add_server" | "memcache_close") => {
+                Some(self.hook_memcache_server(None, function_name, ApiStyle::Procedural))
+            }
+            (Some("Memcache" | "MemcachePool"), f)
+                if MEMCACHE_EMPTY_METHOD_MAPPING.contains_key(f) =>
+            {
+                Some(self.hook_memcache_empty_methods(
+                    class_name.map(ToOwned::to_owned),
+                    function_name,
+                    ApiStyle::OO,
+                ))
+            }
+            (None, f) if MEMCACHE_EMPTY_METHOD_MAPPING.contains_key(&f["memcache_".len()..]) => {
+                Some(self.hook_memcache_empty_methods(None, function_name, ApiStyle::Procedural))
+            }
+            (Some("Memcache" | "MemcachePool"), f)
+                if MEMCACHE_KEY_METHOD_MAPPING.contains_key(f) =>
+            {
+                Some(self.hook_memcache_key_methods(
+                    class_name.map(ToOwned::to_owned),
+                    function_name,
+                    ApiStyle::OO,
+                ))
+            }
+            (None, f) if MEMCACHE_KEY_METHOD_MAPPING.contains_key(&f["memcache_".len()..]) => {
+                Some(self.hook_memcache_key_methods(None, function_name, ApiStyle::Procedural))
+            }
+            _ => None,
+        }
+    }
+}
+
+impl MemcachePlugin {
+    fn hook_memcache_server(
+        &self, class_name: Option<String>, function_name: String, style: ApiStyle,
+    ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        (
+            Box::new(move |_, execute_data| {
+                let this = style.get_this_mut(execute_data)?;
+                let handle = this.handle();
+                PEER_MAP.remove(&handle);
+
+                debug!(
+                    handle,
+                    ?class_name,
+                    function_name,
+                    "remove peers cache when server added"
+                );
+
+                Ok(Box::new(()))
+            }),
+            Noop::noop(),
+        )
+    }
+
+    #[instrument(skip_all)]
+    fn hook_memcache_empty_methods(
+        &self, class_name: Option<String>, function_name: String, style: ApiStyle,
+    ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        (
+            Box::new(move |request_id, execute_data| {
+                let tag_info = MEMCACHE_EMPTY_METHOD_MAPPING
+                    .get(&*get_tag_key(class_name.as_deref(), &function_name))
+                    .unwrap();
+
+                let this = style.get_this_mut(execute_data)?;
+                let peer = get_peer(this);
+
+                let span = create_exit_span(
+                    style,
+                    request_id,
+                    class_name.as_deref(),
+                    &function_name,
+                    &peer,
+                    tag_info,
+                    None,
+                )?;
+
+                Ok(Box::new(span))
+            }),
+            Box::new(after_hook),
+        )
+    }
+
+    #[instrument(skip_all)]
+    fn hook_memcache_key_methods(
+        &self, class_name: Option<String>, function_name: String, style: ApiStyle,
+    ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        (
+            Box::new(move |request_id, execute_data| {
+                let tag_info = MEMCACHE_KEY_METHOD_MAPPING
+                    .get(&*get_tag_key(class_name.as_deref(), &function_name))
+                    .unwrap();
+
+                let key = style
+                    .get_mut_parameter(execute_data, 0)
+                    .as_z_str()
+                    .and_then(|s| s.to_str().ok())
+                    .map(ToOwned::to_owned)
+                    .unwrap_or_default();
+
+                let this = style.get_this_mut(execute_data)?;
+                let peer = get_peer(this);
+
+                let span = create_exit_span(
+                    style,
+                    request_id,
+                    class_name.as_deref(),
+                    &function_name,
+                    &peer,
+                    tag_info,
+                    Some(&key),
+                )?;
+
+                Ok(Box::new(span))
+            }),
+            Box::new(after_hook),
+        )
+    }
+}
+
+#[instrument(skip_all)]
+fn after_hook(
+    _: Option<i64>, span: Box<dyn Any>, _: &mut ExecuteData, return_value: &mut ZVal,
+) -> crate::Result<()> {
+    let mut span = span.downcast::<Span>().expect("Downcast to Span failed");
+
+    if let Some(b) = return_value.as_bool() {
+        if !b {
+            span.span_object_mut().is_error = true;
+        }
+    }
+
+    log_exception(&mut *span);
+
+    Ok(())
+}
+
+fn create_exit_span(
+    style: ApiStyle, request_id: Option<i64>, class_name: Option<&str>, function_name: &str,
+    remote_peer: &str, tag_info: &TagInfo<'_>, key: Option<&str>,
+) -> anyhow::Result<Span> {
+    RequestContext::try_with_global_ctx(request_id, |ctx| {
+        let mut span = ctx.create_exit_span(
+            &style.generate_peer_name(class_name, function_name),
+            remote_peer,
+        );
+
+        let span_object = span.span_object_mut();
+        span_object.set_span_layer(SpanLayer::Cache);
+        span_object.component_id = COMPONENT_PHP_MEMCACHED_ID;
+        span_object.add_tag(TAG_CACHE_TYPE, "memcache");
+        if let Some(cmd) = tag_info.cmd {
+            span_object.add_tag(TAG_CACHE_CMD, cmd);
+        }
+        if let Some(op) = &tag_info.op {
+            span_object.add_tag(TAG_CACHE_OP, op.to_string());
+        };
+        if let Some(key) = key {
+            span_object.add_tag(TAG_CACHE_KEY, key)
+        }
+
+        Ok(span)
+    })
+}
+
+fn get_peer(this: &mut ZObj) -> String {
+    let handle = this.handle();
+
+    PEER_MAP
+        .entry(handle)
+        .or_insert_with(|| {
+            debug!(
+                handle,
+                "start to call {:?}::getExtendedStats method",
+                this.get_class().get_name()
+            );
+            let stats = match this.call("getExtendedStats", []) {
+                Ok(stats) => stats,
+                Err(err) => {
+                    error!(
+                        ?err,
+                        "call {:?}::getExtendedStats method failed",
+                        this.get_class().get_name()
+                    );
+                    return "".to_owned();
+                }
+            };
+
+            stats
+                .as_z_arr()
+                .map(|arr| {
+                    arr.iter()
+                        .map(|(key, _)| match key {
+                            IterKey::Index(i) => i.to_string(),
+                            IterKey::ZStr(s) => s.to_str().unwrap_or_default().to_string(),
+                        })
+                        .collect::<Vec<_>>()
+                        .join(",")
+                })
+                .unwrap_or_default()
+        })
+        .value()
+        .clone()
+}
+
+fn get_tag_key(class_name: Option<&str>, function_name: &str) -> String {
+    match class_name {
+        Some(_) => function_name.to_ascii_lowercase(),
+        None => function_name["memcache_".len()..].to_string(),
+    }
+}
diff --git a/src/plugin/style.rs b/src/plugin/style.rs
new file mode 100644
index 0000000..d06b828
--- /dev/null
+++ b/src/plugin/style.rs
@@ -0,0 +1,68 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::execute::{get_this_mut, validate_num_args};
+use anyhow::Context;
+use phper::{
+    objects::ZObj,
+    values::{ExecuteData, ZVal},
+};
+
+/// Api style.
+#[derive(Clone, Copy)]
+pub enum ApiStyle {
+    /// Object-oriented.
+    OO,
+    /// Procedural.
+    Procedural,
+}
+
+impl ApiStyle {
+    pub fn get_this_mut(self, execute_data: &mut ExecuteData) -> anyhow::Result<&mut ZObj> {
+        match self {
+            ApiStyle::OO => get_this_mut(execute_data),
+            ApiStyle::Procedural => execute_data
+                .get_mut_parameter(0)
+                .as_mut_z_obj()
+                .context("first argument isn't object"),
+        }
+    }
+
+    pub fn get_mut_parameter(self, execute_data: &mut ExecuteData, index: usize) -> &mut ZVal {
+        let index = match self {
+            ApiStyle::OO => index,
+            ApiStyle::Procedural => index + 1,
+        };
+        execute_data.get_mut_parameter(index)
+    }
+
+    #[allow(dead_code)]
+    pub fn validate_num_args(
+        self, execute_data: &mut ExecuteData, num: usize,
+    ) -> anyhow::Result<()> {
+        let num = match self {
+            ApiStyle::OO => num,
+            ApiStyle::Procedural => num + 1,
+        };
+        validate_num_args(execute_data, num)
+    }
+
+    pub fn generate_peer_name(self, class_name: Option<&str>, function_name: &str) -> String {
+        match self {
+            ApiStyle::OO => format!("{}->{}", class_name.unwrap_or_default(), function_name),
+            ApiStyle::Procedural => function_name.to_owned(),
+        }
+    }
+}
diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml
index 3b45f41..3729f39 100644
--- a/tests/data/expected_context.yaml
+++ b/tests/data/expected_context.yaml
@@ -15,7 +15,7 @@
 
 segmentItems:
   - serviceName: skywalking-agent-test-1
-    segmentSize: 18
+    segmentSize: 19
     segments:
       - segmentId: "not null"
         spans:
@@ -1253,6 +1253,151 @@
               - { key: url, value: "http://127.0.0.1:9011/mongodb.php" }
               - { key: http.method, value: GET }
               - { key: http.status_code, value: "200" }
+      - segmentId: "not null"
+        spans:
+          - operationName: MemcachePool->set
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: set }
+              - { key: cache.op, value: write }
+              - { key: cache.key, value: foo }
+          - operationName: MemcachePool->set
+            parentSpanId: 0
+            spanId: 2
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: set }
+              - { key: cache.op, value: write }
+              - { key: cache.key, value: bar }
+          - operationName: MemcachePool->get
+            parentSpanId: 0
+            spanId: 3
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: foo }
+          - operationName: MemcachePool->get
+            parentSpanId: 0
+            spanId: 4
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: bar }
+          - operationName: MemcachePool->get
+            parentSpanId: 0
+            spanId: 5
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: true
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: not-exists }
+          - operationName: memcache_set
+            parentSpanId: 0
+            spanId: 6
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: set }
+              - { key: cache.op, value: write }
+              - { key: cache.key, value: foo }
+          - operationName: memcache_get
+            parentSpanId: 0
+            spanId: 7
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: foo }
+          - operationName: memcache_get
+            parentSpanId: 0
+            spanId: 8
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: true
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: not-exists }
+          - operationName: GET:/memcache.php
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8001
+            isError: false
+            spanType: Entry
+            peer: ""
+            skipAnalysis: false
+            tags:
+              - { key: url, value: "http://127.0.0.1:9011/memcache.php" }
+              - { key: http.method, value: GET }
+              - { key: http.status_code, value: "200" }
   - serviceName: skywalking-agent-test-2
     segmentSize: 1
     segments:
@@ -1375,7 +1520,7 @@
               - { key: http.method, value: GET }
               - { key: http.status_code, value: "200" }
   - serviceName: skywalking-agent-test-2-swoole
-    segmentSize: 9
+    segmentSize: 10
     segments:
       - segmentId: "not null"
         spans:
@@ -1779,3 +1924,52 @@
               - { key: url, value: "http://127.0.0.1:9502/mongodb" }
               - { key: http.method, value: GET }
               - { key: http.status_code, value: "200" }
+      - segmentId: "not null"
+        spans:
+          - operationName: MemcachePool->set
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: set }
+              - { key: cache.op, value: write }
+              - { key: cache.key, value: foo000 }
+          - operationName: MemcachePool->get
+            parentSpanId: 0
+            spanId: 2
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: foo000 }
+          - operationName: GET:/memcache
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8001
+            isError: false
+            spanType: Entry
+            peer: ""
+            skipAnalysis: false
+            tags:
+              - { key: url, value: "http://127.0.0.1:9502/memcache" }
+              - { key: http.method, value: GET }
+              - { key: http.status_code, value: "200" }
diff --git a/tests/e2e.rs b/tests/e2e.rs
index 90ad6e3..9c8854b 100644
--- a/tests/e2e.rs
+++ b/tests/e2e.rs
@@ -57,6 +57,7 @@
     request_fpm_redis().await;
     request_fpm_rabbitmq().await;
     request_fpm_mongodb().await;
+    request_fpm_memcache().await;
     request_swoole_curl().await;
     request_swoole_2_curl().await;
     request_swoole_2_pdo().await;
@@ -65,6 +66,7 @@
     request_swoole_2_redis().await;
     request_swoole_2_predis().await;
     request_swoole_2_mongodb().await;
+    request_swoole_2_memcache().await;
     sleep(Duration::from_secs(3)).await;
     request_collector_validate().await;
 }
@@ -150,6 +152,14 @@
     .await;
 }
 
+async fn request_fpm_memcache() {
+    request_common(
+        HTTP_CLIENT.get(format!("http://{}/memcache.php", PROXY_SERVER_1_ADDRESS)),
+        "ok",
+    )
+    .await;
+}
+
 async fn request_swoole_curl() {
     request_common(
         HTTP_CLIENT.get(format!("http://{}/curl", SWOOLE_SERVER_1_ADDRESS)),
@@ -214,6 +224,14 @@
     .await;
 }
 
+async fn request_swoole_2_memcache() {
+    request_common(
+        HTTP_CLIENT.get(format!("http://{}/memcache", SWOOLE_SERVER_2_ADDRESS)),
+        "ok",
+    )
+    .await;
+}
+
 async fn request_collector_validate() {
     request_common(
         HTTP_CLIENT
diff --git a/tests/php/fpm/memcache.php b/tests/php/fpm/memcache.php
new file mode 100644
index 0000000..c3d9b78
--- /dev/null
+++ b/tests/php/fpm/memcache.php
@@ -0,0 +1,46 @@
+<?php
+
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+use Webmozart\Assert\Assert;
+
+require_once dirname(__DIR__) . "/vendor/autoload.php";
+
+{
+    $mc = new Memcache();
+    $mc->connect("127.0.0.1", 11211);
+
+    $mc->set("foo", "Hello!");
+    $mc->set("bar", "Memcached...");
+
+    Assert::same($mc->get("foo"), 'Hello!');
+    Assert::same($mc->get("bar"), "Memcached...");
+
+    Assert::false($mc->get("not-exists"));
+}
+
+{
+    $mc = memcache_connect("127.0.0.1", 11211);
+
+    memcache_set($mc, "foo", "Hello!!");
+
+    Assert::same(memcache_get($mc, "foo"), 'Hello!!');
+
+    Assert::false(memcache_get($mc, "not-exists"));
+}
+
+echo "ok";
diff --git a/tests/php/swoole/main.2.php b/tests/php/swoole/main.2.php
index 9d8f0d8..8117da8 100644
--- a/tests/php/swoole/main.2.php
+++ b/tests/php/swoole/main.2.php
@@ -106,6 +106,16 @@
             }
             break;
 
+        case '/memcache':
+            {
+                $mc = new Memcache();
+                $mc->addServer("127.0.0.1", 11211);
+
+                $mc->set("foo000", "bar000");
+                Assert::same($mc->get("foo000"), 'bar000');
+            }
+            break;
+
         default:
             throw new DomainException("Unknown operation");
         }