Commit 7b4d4a9f authored by luhongguang's avatar luhongguang

add:test

parent 067601ba
<?php
namespace App\Exception\custom;
use App\Exception\BaseException;
class GoodsException extends BaseException
{
protected $base_code = Code::GOODS;
protected $cus = [
0 => '商品创建失败,请稍后重试',
1 => '店铺ID创建失败',
2 => '商品ID创建失败',
3 => '门店ID创建失败',
4 => '门店名字不能重复',
5 => '套餐编辑失败,请稍后重试',
6 => '套餐ID创建失败',
7 => '套餐名字不能重复',
8 => '套餐数据不全',
9 => '审核驳回时,驳回原因不能为空',
10 => '当前商品已经审核过了,不需要重复审核',
11 => '审核商品失败',
12 => '审核状态错误',
13 => '无权限操作',
];
}
\ No newline at end of file
<?php
namespace Validate;
/**
* Class EditSetmealValidate
*
* @package Validate
*/
class EditSetmealValidate extends BaseValidate
{
protected $rule = [
'life_account_id' => 'require',
'good_sku_id' => 'require',
'setmeal_data' => 'require',
];
protected $message = [
"life_account_id" => "生活号id不能为空",
"good_sku_id" => "商品id不能为空",
"setmeal_data" => "套餐数据不能为空",
];
}
\ No newline at end of file
<?php
namespace Validate;
/**
* Class EditStoresValidate
*
* @package Validate
*/
class EditStoresValidate extends BaseValidate
{
protected $rule = [
'life_account_id' => 'require',
'name' => 'require',
'address_lng' => 'require',
'address_lat' => 'require',
'address' => 'require',
'phone' => 'require',
];
protected $message = [
"life_account_id" => "生活号id不能为空",
"name" => "门店名称不能为空",
"address_lng" => "门店位置经度不能为空",
"address_lat" => "门店位置纬度不能为空",
"address" => "门店地址不能为空",
"phone" => "门店电话不能为空",
];
}
\ No newline at end of file
<?php
namespace Validate;
/**
* Class GoodsAuditValidate
*
* @package Validate
*/
class GoodsAuditValidate extends BaseValidate
{
protected $rule = [
'goods_spu_id' => 'require',
'status' => 'require',
'uid' => 'require',
'username' => 'require',
];
protected $message = [
"goods_spu_id" => "商品id不能为空",
"status" => "商品状态不能为空",
"uid" => "用户id不能为空",
"username" => "用户username不能为空",
];
}
\ No newline at end of file
<?php
namespace Validate;
/**
* Class SetmealListValidate
*
* @package Validate
*/
class SetmealListValidate extends BaseValidate
{
protected $rule = [
'goods_sku_id' => 'require',
];
protected $message = [
"goods_sku_id" => "商品id不能为空",
];
}
\ No newline at end of file
<?php
namespace Validate;
/**
* Class StoresListValidate
*
* @package Validate
*/
class StoresListValidate extends BaseValidate
{
protected $rule = [
'life_account_id' => 'require',
];
protected $message = [
"life_account_id" => "生活号id不能为空",
];
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class Category
* 商品分类
* @package App\Models\goods\mysql
*/
class Category extends MysqlBase
{
const TABLE_NAME = 'category';
const CONFIG_INDEX = 'goods';
const LEVEL_1 = 1;
const LEVEL_2 = 2;
public static function getRecordList($where = [], $columns = [])
{
if (empty($columns)) {
$columns = '*';
}
return self::select($columns, $where);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class GoodsOperationRecord
* 商品操作记录
* @package App\Models\goods\mysql
*/
class GoodsOperationRecord extends MysqlBase
{
const TABLE_NAME = 'goods_operation_record';
const CONFIG_INDEX = 'goods';
const STATUS_AUDIT = 0;//待审核
const STATUS_PASS = 1;//审核通过
const STATUS_REJECT = 2;//审核驳回
const ONLINE_STATUS_ONLINE = 1;//已上架
const ONLINE_STATUS_OFFLINE = 2;//已下架
public static function getRecord($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::get($colums, $where);
}
public static function insertRecord($colums)
{
return self::insert($colums);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class GoodsSku
* 商品sku信息
* @package App\Models\goods\mysql
*/
class GoodsSku extends MysqlBase
{
const TABLE_NAME = 'goods_sku';
const CONFIG_INDEX = 'goods';
const STATUS_AUDIT = 0;//待审核
const STATUS_PASS = 1;//审核通过
const STATUS_REJECT = 2;//审核驳回
const ONLINE_STATUS_ONLINE = 1;//已上架
const ONLINE_STATUS_OFFLINE = 2;//已下架
public static function getRecord($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::get($colums, $where);
}
public static function getRecordMaster($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::selectMaster($colums, $where);
}
public static function save($data, $where = [])
{
if (empty($where)) {
return self::insert($data);
}
return self::update($data, $where);
}
public static function deleteRecord($where)
{
return self::delete($where);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class GoodsSkuStores
* 商品门店关系
* @package App\Models\goods\mysql
*/
class GoodsSkuStores extends MysqlBase
{
const TABLE_NAME = 'goods_sku_stores';
const CONFIG_INDEX = 'goods';
public static function getRecord($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::get($colums, $where);
}
public static function getRecordMaster($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::selectMaster($colums, $where);
}
public static function save($data, $where = [])
{
if (empty($where)) {
return self::insert($data);
}
return self::update($data, $where);
}
public static function deleteRecord($where)
{
return self::delete($where);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class GoodsSnapshot
* 商品快照
* @package App\Models\goods\mysql
*/
class GoodsSnapshot extends MysqlBase
{
const TABLE_NAME = 'goods_snapshot';
const CONFIG_INDEX = 'goods';
public static function getRecord($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::get($colums, $where);
}
public static function getRecordMaster($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::selectMaster($colums, $where);
}
public static function insertRecord($colums)
{
return self::insert($colums);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class GoodsSpu
* 商品spu信息
* @package App\Models\goods\mysql
*/
class GoodsSpu extends MysqlBase
{
const TABLE_NAME = 'goods_spu';
const CONFIG_INDEX = 'goods';
public static function getRecord($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::get($colums, $where);
}
public static function getRecordMaster($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::selectMaster($colums, $where);
}
public static function insertRecord($colums)
{
return self::insert($colums);
}
public static function updateRecord($colums, $where)
{
return self::update($colums, $where);
}
public static function save($data, $where = [])
{
if (empty($where)) {
return self::insert($data);
}
return self::update($data, $where);
}
public static function deleteRecord($where)
{
return self::delete($where);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class InventoryOperationRecord
* 库存操作记录
* @package App\Models\goods\mysql
*/
class InventoryOperationRecord extends MysqlBase
{
const TABLE_NAME = 'inventory_operation_record';
const CONFIG_INDEX = 'goods';
public static function getRecord($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::get($colums, $where);
}
public static function getRecordMaster($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::selectMaster($colums, $where);
}
public static function insertRecord($colums)
{
return self::insert($colums);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class Setmeal
* 套餐
* @package App\Models\goods\mysql
*/
class Setmeal extends MysqlBase
{
const TABLE_NAME = 'setmeal';
const CONFIG_INDEX = 'goods';
public static function getRecord($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::get($colums, $where);
}
public static function getRecordMaster($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::selectMaster($colums, $where);
}
public static function save($data, $where = [])
{
if (empty($where)) {
return self::insert($data);
}
return self::update($data, $where);
}
public static function deleteRecord($where)
{
return self::delete($where);
}
public static function getAll($goodsSkuId)
{
return self::select('*', ['goods_sku_id' => $goodsSkuId]);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class Shop
* 店铺
* @package App\Models\goods\mysql
*/
class Shop extends MysqlBase
{
const TABLE_NAME = 'shop';
const CONFIG_INDEX = 'goods';
const STATUS_ONLINE = 1;// 上线
const STATUS_OFFLINE = 2;// 下线
public static function getRecord($where, $columns = [])
{
if (empty($columns)) {
$columns = '*';
}
return self::get($columns, $where);
}
public static function getRecordMaster($where, $columns = [])
{
if (empty($columns)) {
$columns = '*';
}
return self::selectMaster($columns, $where);
}
public static function save($data, $where = [])
{
if (empty($where)) {
return self::insert($data);
}
return self::update($data, $where);
}
public static function deleteRecord($where)
{
return self::delete($where);
}
}
\ No newline at end of file
<?php
namespace App\Models\goods\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
/**
* Class Stores
* 门店
* @package App\Models\goods\mysql
*/
class Stores extends MysqlBase
{
const TABLE_NAME = 'stores';
const CONFIG_INDEX = 'goods';
public static function getRecord($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::get($colums, $where);
}
public static function getRecordMaster($where, $colums = [])
{
if (empty($colums)) {
$colums = '*';
}
return self::selectMaster($colums, $where);
}
public static function save($data, $where = [])
{
if (empty($where)) {
return self::insert($data);
}
return self::update($data, $where);
}
public static function deleteRecord($where)
{
return self::delete($where);
}
public static function getAll($lifeAccountId)
{
return self::select('*', ['life_account_id' => $lifeAccountId]);
}
}
\ No newline at end of file
<?php
use App\Base\Base;
use App\Services\goods\GoodsService;
use App\Services\goods\CategoryService;
use App\Services\goods\StoresService;
use App\Services\goods\SetmealService;
use \Validate\EditStoresValidate;
use \Validate\StoresListValidate;
use \Validate\EditSetmealValidate;
use \Validate\SetmealListValidate;
use \Validate\GoodsAuditValidate;
class GoodsController extends Base
{
/**
* 获取商品分类列表
*/
public function get_goods_category_listAction()
{
$categoryList = CategoryService::getCategoryList();
$this->success(["data" => $categoryList]);
}
/**
* 增加商品
*/
public function addAction()
{
$lifeAccountId = 111;
$params = [
"name" => "商品name",
"url" => "url1, url2, url3",
"desc" => "描述描述",
"category_1_id" => 1,
"category_2_id" => 13,
"expiration_time" => "2021-06-10 12:20:30",
"rule_limit" => 3,
"rule_date_type" => 1,
"rule_start_time" => "2021-06-11 12:20:30",
"rule_end_time" => "2021-06-12 12:20:30",
"rule_desc" => "规则描述",
"inventory_total" => 10,
"original_price" => 1500,
"price" => 500,
];
GoodsService::addGoods($lifeAccountId, $params);
$this->success();
}
/**
* 编辑商品
*/
public function editAction()
{
}
/**
* 商品详情
*/
public function infoAction()
{
}
/**
* 上下架
*/
public function online_offlineAction()
{
}
/**
* 审核通过/驳回
*/
public function auditAction()
{
(new GoodsAuditValidate())->validate();
$params = $this->params;
GoodsService::audit($params);
$this->success();
}
/**
* 判断商品名称是否可用
*/
public function check_goods_nameAction()
{
}
/**
* 套餐列表
* todo::可能会去掉
*/
public function setmeal_listAction()
{
(new SetmealListValidate())->validate();
$params = $this->params;
$goodSkuId = $params["goods_sku_id"];
$list = SetmealService::getSetmealList($goodSkuId);
$this->success(["data"=>$list]);
}
/**
* 编辑套餐, 只支持编辑,不支持增加(因为增加时候还没有skuid)
* 这里套餐能一次传多个过来,这里按套餐数组处理
*/
public function edit_setmealAction()
{
(new EditSetmealValidate())->validate();
$params = $this->params;
SetmealService::editSetmeal($params);
$this->success();
}
/**
* 门店列表
* @throws \App\Exception\custom\ParamException
*/
public function stores_listAction()
{
(new StoresListValidate())->validate();
$params = $this->params;
$lifeAccountId = $params["life_account_id"];
$list = StoresService::getStoresList($lifeAccountId);
$this->success(["data"=>$list]);
}
/**
* 编辑门店
*/
public function edit_storesAction()
{
(new EditStoresValidate())->validate();
$params = $this->params;
StoresService::editStores($params);
$this->success();
}
/**
* op后台:商品列表 (spu list)
*/
public function goods_list_opAction()
{
}
/**
* b端:商品列表(spu list)
*/
public function goods_list_bAction()
{
}
/**
* c端 es:商品列表 (sku list)
*/
public function goods_list_cAction()
{
}
}
\ No newline at end of file
<?php
namespace App\Services\goods;
use App\Models\goods\mysql\Category;
class CategoryService
{
public static function getCategoryList()
{
//todo::加上redis
$allCaregoryList = Category::getRecordList([], ['category_id', 'parent_category_id', 'name', 'level']);
$data = [];
if (!empty($allCaregoryList)) {
foreach ($allCaregoryList as $caregory) {
if ($caregory["level"] == Category::LEVEL_1) {
$data["level_1"][] = [
"category_id" => $caregory["category_id"],
"name" => $caregory["name"]
];
}
if ($caregory["level"] == Category::LEVEL_2) {
$data["level_2"][$caregory["parent_category_id"]][] = [
"category_id" => $caregory["category_id"],
"name" => $caregory["name"]
];
}
}
}
return $data;
}
}
\ No newline at end of file
<?php
namespace App\Services\goods;
use Api\PhpServices\Idgen\Idgen;
use App\Exception\custom\GoodsException;
use App\Models\goods\mysql\GoodsOperationRecord;
use App\Models\goods\mysql\GoodsSku;
use App\Models\goods\mysql\GoodsSpu;
use App\Models\goods\mysql\Shop;
class GoodsService
{
/**
* 通过发号器拿 id (非雪花)
* number,获取店铺shop_id用生活号id后两位,商品相关的这里是shop_id的后两位
* @param $number
* @param $type
* @param int $count
* @return array|mixed
*/
public static function getIdgenId($number, $type, $count = 1)
{
$res = Idgen::get(appConfig('idgen.partner'), appConfig('idgen.key'), [], [[
"type" => $type,
'number' => (int)$number,
"count" => $count]]);
return $res['id_datetime'][$type] ?? [];
}
public static function getSnowIdgenId($type, $count = 1)
{
$res = Idgen::get(appConfig('idgen.partner'), appConfig('idgen.key'), [[
"type" => $type,
"count" => $count]], []);
return $res['id_snow'][$type] ?? [];
}
/**
* 添加商品
* @param $lifeAccountId
* @param array $params
* @return mixed
* @throws GoodsException
*/
public static function addGoods($lifeAccountId, $params = [])
{
//todo:: 获取 $lifeAccount信息
$lifeAccount = [
"life_account_id" => 111,
"merchant_id" => 1,
"life_account_name" => "测试测试",
];
GoodsSpu::beginTransaction();
$shopId = self::addShop($lifeAccount);
$spuData = [
"name" => $params["name"],
"url" => $params["url"],
"desc" => $params["desc"],
];
$spuId = self::addGoodsSpu($shopId, $spuData);
$skuId = self::addGoodsSku($spuId, $shopId, $lifeAccountId, $params);
if (!GoodsSpu::commit()) {
GoodsSpu::rollback();
throw new GoodsException(["cus" => 0]);
}
return $skuId;
}
/**
* 添加spu
* @param $shopId
* @param array $spuData
* @return mixed
* @throws GoodsException
*/
private static function addGoodsSpu($shopId, $spuData = [])
{
$res = self::getIdgenId(substr($shopId, -2), "goods");
if (empty($res)) {
throw new GoodsException(['cus' => 2]);
}
$spuId = $res[0];
GoodsSpu::save([
"goods_spu_id" => $spuId,
"shop_id" => $shopId,
"name" => $spuData["name"],
"url" => $spuData["url"],
"desc" => $spuData["desc"],
]);
return $spuId;
}
/**
* 添加sku
* @param $spuId
* @param $shopId
* @param $lifeAccountId
* @param array $skuData
* @return mixed
* @throws GoodsException
*/
private static function addGoodsSku($spuId, $shopId, $lifeAccountId, $skuData = [])
{
$res = self::getIdgenId(substr($shopId, -2), "goods");
if (empty($res)) {
throw new GoodsException(['cus' => 2]);
}
$skuId = $res[0];
GoodsSku::save([
"goods_spu_id" => $spuId,
"goods_sku_id" => $skuId,
"shop_id" => $shopId,
"life_account_id" => $lifeAccountId,
"category_1_id" => $skuData["category_1_id"],
"category_2_id" => $skuData["category_2_id"],
"name" => $skuData["name"],
"url" => $skuData["url"],
"desc" => $skuData["desc"],
"expiration_time" => $skuData["expiration_time"],
"rule_limit" => $skuData["rule_limit"],
"rule_date_type" => $skuData["rule_date_type"],
"rule_start_time" => $skuData["rule_start_time"],
"rule_end_time" => $skuData["rule_end_time"],
"rule_desc" => $skuData["rule_desc"],
"inventory_total" => $skuData["inventory_total"],
"original_price" => $skuData["original_price"],
"price" => $skuData["price"],
]);
return $skuId;
}
/**
* 添加商铺
* @param $lifeAccount
* @return mixed
* @throws GoodsException
*/
private static function addShop($lifeAccount)
{
$lifeAccountId = $lifeAccount["life_account_id"];
$shop = Shop::getRecordMaster(['life_account_id' => $lifeAccountId]);
if (empty($shop)) {
$res = self::getIdgenId(substr($lifeAccountId, -2), "goods");
if (empty($res)) {
throw new GoodsException(['cus' => 1]);
}
$shopId = $res[0];
Shop::save([
"shop_id" => $shopId,
"life_account_id" => $lifeAccountId,
"merchant_id" => $lifeAccount["merchant_id"],
"name" => $lifeAccount["life_account_name"],
"status" => Shop::STATUS_ONLINE,
]);
} else {
$shopId = $shop[0]["shop_id"];
}
return $shopId;
}
/**
* 审核
* @param array $params
* @return bool
* @throws GoodsException
*/
public static function audit($params = [])
{
$goodsSpuId = $params["goods_spu_id"];
$status = $params["status"];
$operatorId = $params["uid"];
$operatorName = $params["username"];
$goodsSpu = GoodsSpu::get("*", ["goods_spu_id" => $goodsSpuId]);
//todo::权限判断 商品所属 life_account_id 或者 后台管理员
//权限判断
if (!empty($params["life_account_id"]) && $goodsSpu["life_account_id"] != $params["life_account_id"]) {
throw new GoodsException(["cus" => 13]);
}
//审核状态是否正确
if (!in_array($status, [GoodsSku::STATUS_PASS, GoodsSku::STATUS_REJECT])) {
throw new GoodsException(["cus" => 12]);
}
//驳回时候要有原因
if ($status == GoodsSku::STATUS_REJECT && empty($params["rejected_reason"])) {
throw new GoodsException(["cus" => 9]);
}
GoodsSku::beginTransaction();
$goodsSkuList = GoodsSku::select("*", ["goods_spu_id" => $goodsSpuId]);
if (!empty($goodsSkuList)) {
foreach ($goodsSkuList as $sku) {
if ($sku["status"] == GoodsSku::STATUS_PASS || $sku["status"] == GoodsSku::STATUS_REJECT) {
throw new GoodsException(["cus" => 10]);
}
$goodsSkuId = $sku["goods_sku_id"];
GoodsSku::save(["status" => $status], ["goods_sku_id" => $sku["goods_sku_id"]]);
//todo:: info to es
//商品操作记录
$res = self::getSnowIdgenId("goods");
if (empty($res)) {
throw new GoodsException(['cus' => 2]);
}
$record = [
"goods_operation_record_id" => $res[0],
"goods_spu_id" => $goodsSpuId,
"goods_sku_id" => $goodsSkuId,
"operator_id" => $operatorId,
"operator_name" => $operatorName,
"goods_status" => $status,
"online_status" => $sku["online_status"],
"before_version" => $sku["version"],
"after_version" => $sku["version"],
"note" => empty($params["rejected_reason"]) ? "" : $params["rejected_reason"],
];
GoodsOperationRecord::insertRecord($record);
}
}
if (!GoodsSku::commit()) {
GoodsSku::rollback();
throw new GoodsException(["cus" => 11]);
}
return true;
}
public static function checkGoodsName($goodsName)
{
}
}
\ No newline at end of file
<?php
namespace App\Services\goods;
use App\Exception\custom\GoodsException;
use App\Models\goods\mysql\GoodsSpu;
use App\Models\goods\mysql\Setmeal;
use App\Models\goods\mysql\Stores;
class SetmealService
{
/**
* @param array $params
* 编辑套餐
* @return \Api\PhpUtils\Mysql\MysqlBase
* @throws GoodsException
*/
public static function editSetmeal($params = [])
{
$lifeAccountId = $params["life_account_id"];
$goodsSkuId = $params["good_sku_id"];
$setmealData = json_decode($params['setmeal_data'], true);
Setmeal::beginTransaction();
$updateData = $insertData = [];
foreach ($setmealData as $key => $setmeal) {
if (empty($setmeal["name"]) || empty($setmeal["unit"]) || empty($setmeal["unit_number"]) || empty($setmeal["unit_price"])) {
throw new GoodsException(['cus' => 8]);
}
if (!empty($setmeal["setmeal_id"])) {
$updateData[] = [
'setmeal_id' => $setmeal["setmeal_id"],
'life_account_id' => $lifeAccountId,
'goods_sku_id' => $goodsSkuId,
'name' => $setmeal["name"],
'unit' => $setmeal["unit"],
'unit_number' => $setmeal["unit_number"],
'unit_price' => $setmeal["unit_price"] * 100,
];
} else {
$insertData[] = [
'life_account_id' => $lifeAccountId,
'goods_sku_id' => $goodsSkuId,
'name' => $setmeal["name"],
'unit' => $setmeal["unit"],
'unit_number' => $setmeal["unit_number"],
'unit_price' => $setmeal["unit_price"] * 100,
];
}
}
if (!empty($insertData)) {
$ids = GoodsService::getIdgenId(substr($lifeAccountId, -2), "goods", count($insertData));
if (empty($ids)) {
throw new GoodsException(['cus' => 6]);
}
foreach ($insertData as $insertKey => $item) {
//判断重复
$setmeal = Setmeal::getRecordMaster(["goods_sku_id"=>$goodsSkuId, "name"=>$item["name"]]);
if (!empty($setmeal)) {
throw new GoodsException(['cus' => 7]);
}
$item["setmeal_id"] = $ids[$insertKey];
Setmeal::save($item);
}
}
if (!empty($updateData)) {
foreach ($updateData as $item) {
//todo::update去重
$where = [];
$where["setmeal_id"] = $item["setmeal_id"];
unset($item["setmeal_id"]);
Setmeal::save($item, $where);
}
}
if (!Setmeal::commit()) {
Setmeal::rollback();
throw new GoodsException(["cus" => 5]);
}
}
/**
* @param $goodSkuId
* @return \Api\PhpUtils\Mysql\MysqlBase
*/
public static function getSetmealList($goodSkuId)
{
$list = Setmeal::getAll($goodSkuId);
if (!empty($list)) {
foreach ($list as $key => $item) {
$list[$key]["unit_price"] = $item["unit_price"] / 100;
}
}
return $list;
}
}
\ No newline at end of file
<?php
namespace App\Services\goods;
use App\Exception\custom\GoodsException;
use App\Models\goods\mysql\Stores;
class StoresService
{
/**
* @param array $params
* 编辑门店
* @return \Api\PhpUtils\Mysql\MysqlBase
* @throws GoodsException
*/
public static function editStores($params = [])
{
$data = $where = [];
$lifeAccountId = $params["life_account_id"];
$name = $params["name"];
if (!empty($params["stores_id"])) {
$where["stores_id"] = $params["stores_id"];
} else {
$stores = Stores::getRecordMaster(["life_account_id"=>$lifeAccountId, "name"=>$name]);
if (!empty($stores)) {
throw new GoodsException(['cus' => 4]);
}
$res = GoodsService::getIdgenId(substr($lifeAccountId, -2), "goods");
if (empty($res)) {
throw new GoodsException(['cus' => 3]);
}
$data["stores_id"] = $res["0"];
}
$data["name"] = $name;
$data["address_lng"] = $params["address_lng"];
$data["address_lat"] = $params["address_lat"];
$data["address"] = $params["address"];
$data["phone"] = $params["phone"];
$data["life_account_id"] = $params["life_account_id"];
return Stores::save($data, $where);
}
/**
* @param $lifeAccountId
* @return \Api\PhpUtils\Mysql\MysqlBase
*/
public static function getStoresList($lifeAccountId)
{
return Stores::getAll($lifeAccountId);
}
}
\ No newline at end of file
<?php
namespace Api\PhpServices\XhProfiler;
use Api\PhpUtils\Mongo\MongoBase;
class XhMongo extends MongoBase
{
private $domian ;
public function __construct($domain)
{
$this->domian = $domain;
}
protected function getConfigIndex()
{
//需要在yaconf 配置 mongo
return 'xhprof';
}
protected function getDatabaseName()
{
return 'xhprof';
}
protected function getCollectionName()
{
// 根据 domain 来生成对应的collection
return $this->domian.'_'.'xhprof';
}
}
\ No newline at end of file
<?php
namespace Api\PhpServices\XhProfiler;
use Xhgui\Profiler\Profiler;
class XhProfilerService
{
private $profiler;
public function __construct($domain)
{
// 根据域名读取 配置
$this->profiler = new Profiler([]);
}
/**
* 开启 xhprofiler 分析
* @param $domain
*/
public function start($domain): void
{
//根据域名选择开不开启 分析
if ($this->judgeSwitch($domain) === false)
{
return ;
}
//开启 分析器,并且初始化 mongodb 实例
$this->profiler->start();
}
/**
* 结束分析 并且 根据域名存储到不同的 mongodb collection 中
* @param $domain
*/
public function end($domain): void
{
// 结束分析器,生产性能检测数据
$profiler_data = $this->profiler->disable();
//将数据存储到 指定mongodb中
$mongo = new XhMongo('test');
$mongo->insertOne($profiler_data);
// $this->profiler->save($profiler_data);
}
/**
* 开关决策器
* @param $domain
* @return bool
*/
private function judgeSwitch($domain)
{
// 从 yaconf 获取配置 ,判断当前域名是否要开启
return true;
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Ksy;
use Api\PhpUtils\Http\Request;
/**
* 金山云ks3相关
* 仅实现了签名及删除单个方法,其他未实现
* ks3直接访问地址: https://ks3-cn-beijing.ksyun.com/article-yidian/article/0JRvMeHk
*/
class Ks3Util
{
const KS3_BASE_URL = 'ks3-cn-beijing.ksyun.com';
const FALLBACK_BUIDLER_BUCKET = 'article-yidian';
const KS_DATE_SCHEME = 'D, d M Y H:i:s \G\M\T';
const KSYUN_SK = "OMw7UAOkrLVsRevuP44ctAlfzriDzUm9+JvOFoYuiSoLgaLJWi+kjUa+4G3EWXFakQ==";
const KSYUN_AK = "AKLTnATdoXUnR72IDSGgN2WSOQ";
const YIDIAN_PROXY = true;
//删除fallback builder 对于ks3节点的文章
public static function delete_doc($docid, $timeout = 1000)
{
return self::delete(self::FALLBACK_BUIDLER_BUCKET, 'article/'.$docid, $timeout);
}
public static function put_object($data,$bucket,$object_key, $timeout = 1000)
{
$url = self::get_url($bucket).'/'.$object_key;
$canonicalized_resource = self::get_canonicalized_resource($bucket, $object_key);
$headers = array(
'Host' => self::get_url($bucket),
'Date' => gmdate(self::KS_DATE_SCHEME),
'Content-Type' => 'multipart/form-data',
'Referer' => $url,
'x-kss-acl' => 'public-read',
//'Expect' => '100-continue',
);
$headers['Authorization'] = self::sign('PUT', $canonicalized_resource, $headers);
foreach ($headers as $key => $value)
{
$header[] = $key.':'.$value;
}
$request = new Request();
$str = $request->uploadPut($url, $data, $timeout, 1, $header, self::YIDIAN_PROXY);
return $str;
}
//查询文章在ks3是否存在,返回金山云返回的head http状态吗
public static function exist($bucket, $object_key, $timeout = 1000)
{
$url = self::get_url($bucket).'/'.$object_key;
$canonicalized_resource = self::get_canonicalized_resource($bucket, $object_key);
$headers = array(
'Host' => self::get_url($bucket),
'Date' => gmdate(self::KS_DATE_SCHEME),
'Content-Type' => 'application/xml',
);
$headers['Authorization'] = self::sign('HEAD', $canonicalized_resource, $headers);
foreach ($headers as $key => $value)
{
$header[] = $key.':'.$value;
}
$request = new Request();
$str = $request->head($url, $timeout, 1, $header, self::YIDIAN_PROXY);
return $str;
}
//删除ks3 节点指定bucket中的object
private static function delete($bucket, $object_key, $timeout)
{
$url = self::get_url($bucket).'/'.$object_key;
$headers = array(
'Host' => self::get_url($bucket),
'Accept' => '*/*',
'Accept-Encoding' => 'deflate, gzip',
'Referer' => $url,
'Content-Type' => 'application/xml',
'Date' => gmdate(self::KS_DATE_SCHEME),
'Content-Length' => 0,
);
$canonicalized_resource = self::get_canonicalized_resource($bucket, $object_key);
$headers['Authorization'] = self::sign('DELETE', $canonicalized_resource, $headers);
foreach ($headers as $key => $value)
{
$header[] = $key.':'.$value;
}
$request = new Request();
$str = $request->delete($url, $timeout, 1, $header, self::YIDIAN_PROXY);
return $str;
}
private static function get_url($bucket)
{
return $bucket.'.'.self::KS3_BASE_URL;
}
private static function sign($method,$canonicalized_resource, $headers=array(), $query = array())
{
$canonicalized_kss_headers = self::format_headers($headers);
$tosign = $method . "\n"
. '' . "\n"
. $headers['Content-Type'] . "\n"
. $headers['Date'] . "\n"
. $canonicalized_kss_headers
. $canonicalized_resource;
$signature = base64_encode(hash_hmac('sha1', $tosign, self::KSYUN_SK, true));
$signature = 'KSS ' . self::KSYUN_AK . ':' . $signature;
return $signature;
}
//生成签名所需CanonicalizedKssHeaders,详见https://ks3.ksyun.com/doc/api/index.html
private static function format_headers(array $headers)
{
if( empty($headers) )
{
return '';
}
foreach ($headers as $key => $header) {
if( substr($key,0,6) == 'x-kss-' )
{
$headers_to_sign[strtolower($key)] = $header;
}
}
if( empty($headers_to_sign) )
{
return '';
}
ksort($headers_to_sign);
$ret = '';
foreach ($headers_to_sign as $key => $value)
{
$ret = $key.':'.$value."\n";
}
return $ret;
}
//
private static function get_canonicalized_resource($bucket, $object_key)
{
return '/'.$bucket.'/'.$object_key;
}
}
Elan Ruusamäe <glen@pld-linux.org>
Lauri Piisang <lauri.piisang@eesti.ee>
MIT License
Copyright (c) 2017 Lauri Piisang
Copyright (c) 2019-2020 Elan Ruusamäe
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.
This diff is collapsed.
<?php
/**
* This loads all classes used by this project.
*/
require_once __DIR__ . '/src/Profilers/ProfilerInterface.php';
require_once __DIR__ . '/src/Saver/SaverInterface.php';
require_once __DIR__ . '/src/Exception/ProfilerException.php';
require_once __DIR__ . '/src/Profiler.php';
require_once __DIR__ . '/src/ProfilerFactory.php';
require_once __DIR__ . '/src/Profilers/AbstractProfiler.php';
require_once __DIR__ . '/src/Profilers/Tideways.php';
require_once __DIR__ . '/src/Profilers/TidewaysXHProf.php';
require_once __DIR__ . '/src/Profilers/UProfiler.php';
require_once __DIR__ . '/src/Profilers/XHProf.php';
require_once __DIR__ . '/src/ProfilingData.php';
require_once __DIR__ . '/src/ProfilingFlags.php';
require_once __DIR__ . '/src/Saver/AbstractSaver.php';
require_once __DIR__ . '/src/Saver/FileSaver.php';
require_once __DIR__ . '/src/Saver/MongoSaver.php';
require_once __DIR__ . '/src/Saver/PdoSaver.php';
require_once __DIR__ . '/src/Saver/StackSaver.php';
require_once __DIR__ . '/src/Saver/UploadSaver.php';
require_once __DIR__ . '/src/SaverFactory.php';
<?php
/**
* Bootstrap for php-profiler. Copy and customize this file,
* include this file inside some bootstrapper or other "early central point in execution"
*
* Documentation:
* - https://github.com/perftools/php-profiler#create-profiler
* - https://github.com/perftools/php-profiler#config
*/
use Xhgui\Profiler\Profiler;
use Xhgui\Profiler\ProfilingFlags;
require __DIR__ . '/../vendor/autoload.php';
try {
$config = array(
// If defined, use specific profiler
// otherwise use any profiler that's found
'profiler' => Profiler::PROFILER_TIDEWAYS_XHPROF,
// This allows to configure, what profiling data to capture
'profiler.flags' => array(
ProfilingFlags::CPU,
ProfilingFlags::MEMORY,
ProfilingFlags::NO_BUILTINS,
ProfilingFlags::NO_SPANS,
),
// Saver to use.
// Please note that 'pdo' and 'mongo' savers are deprecated
// Prefer 'upload' or 'file' saver.
'save.handler' => Profiler::SAVER_UPLOAD,
// Saving profile data by upload is only recommended with HTTPS
// endpoints that have IP whitelists applied.
// https://github.com/perftools/php-profiler#upload-saver
'save.handler.upload' => array(
'url' => 'https://example.com/run/import',
// The timeout option is in seconds and defaults to 3 if unspecified.
'timeout' => 3,
// the token must match 'upload.token' config in XHGui
'token' => 'token',
),
// https://github.com/perftools/php-profiler#file-saver
'save.handler.file' => array(
// Appends jsonlines formatted data to this path
'filename' => '/tmp/xhgui.data.jsonl',
),
// https://github.com/perftools/php-profiler#stack-saver
'save.handler.stack' => array(
'savers' => array(
Profiler::SAVER_UPLOAD,
Profiler::SAVER_FILE,
),
// if saveAll=false, break the chain on successful save
'saveAll' => false,
),
// https://github.com/perftools/php-profiler#mongodb-saver
'save.handler.mongodb' => array(
'dsn' => 'mongodb://127.0.0.1:27017',
'database' => 'xhprof',
// Allows you to pass additional options like replicaSet to MongoClient.
// 'username', 'password' and 'db' (where the user is added)
'options' => array(),
// Allows you to pass driver options like ca_file to MongoClient
'driverOptions' => array(),
),
// https://github.com/perftools/php-profiler#pdo-saver
'save.handler.pdo' => array(
'dsn' => 'sqlite:/tmp/xhgui.sqlite3',
'user' => null,
'pass' => null,
'table' => 'results',
),
// Environment variables to exclude from profiling data
'profiler.exclude-env' => array(),
'profiler.options' => array(),
/**
* Determine whether the profiler should run.
* This default implementation just disables the profiler.
* Override this with your custom logic in your config
* @return bool
*/
'profiler.enable' => function () {
return true;
},
/**
* Creates a simplified URL given a standard URL.
* Does the following transformations:
*
* - Remove numeric values after "=" in query string.
*
* @param string $url
* @return string
*/
'profiler.simple_url' => function ($url) {
return preg_replace('/=\d+/', '', $url);
},
/**
* Enable this to clean up the url before submitting it to XHGui.
* This way it is possible to remove sensitive data or discard any other data.
*
* The URL argument is the `REQUEST_URI` or `argv` value.
*
* @param string $url
* @return string
*/
'profiler.replace_url' => function ($url) {
return str_replace('token', '', $url);
},
);
/**
* The constructor will throw an exception if the environment
* isn't fit for profiling (extensions missing, other problems)
*/
$profiler = new Profiler($config);
// The profiler itself checks whether it should be enabled
// for request (executes lambda function from config)
$profiler->start();
} catch (Exception $e) {
// throw away or log error about profiling instantiation failure
error_log($e->getMessage());
}
<?php
namespace Xhgui\Profiler\Exception;
use RuntimeException;
class ProfilerException extends RuntimeException
{
}
<?php
namespace Xhgui\Profiler;
use Xhgui\Profiler\Exception\ProfilerException;
use Xhgui\Profiler\Profilers\ProfilerInterface;
use Xhgui\Profiler\Saver\SaverInterface;
class Profiler
{
const SAVER_UPLOAD = 'upload';
const SAVER_FILE = 'file';
const SAVER_MONGODB = 'mongodb';
const SAVER_PDO = 'pdo';
const SAVER_STACK = 'stack';
const PROFILER_TIDEWAYS = 'tideways';
const PROFILER_TIDEWAYS_XHPROF = 'tideways_xhprof';
const PROFILER_UPROFILER = 'uprofiler';
const PROFILER_XHPROF = 'xhprof';
/**
* Profiler configuration.
*
* @var array
*/
private $config;
/**
* @var SaverInterface
*/
private $saveHandler;
/**
* @var ProfilerInterface
*/
private $profiler;
/**
* Simple state variable to hold the value of 'Is the profiler running or not?'
*
* @var bool
*/
private $running;
/**
* If true, session is closed, buffers are flushed and fcgi request is finished on shutdown handler
* Disable this if this conflicts with your framework.
*
* @var bool
*/
private $flush;
/**
* Profiler constructor.
*
* @param array $config
*/
public function __construct(array $config)
{
$this->config = array_replace($this->getDefaultConfig(), $config);
}
/**
* Evaluate profiler.enable condition, and start profiling if that returned true.
*/
public function start($flush = true)
{
if (!$this->shouldRun()) {
return;
}
$this->enable();
$this->flush = $flush;
// shutdown handler collects and stores the data.
$this->registerShutdownHandler();
}
/**
* Stop profiling. Get currently collected data and save it
*/
public function stop()
{
$data = $this->disable();
$this->save($data);
return $data;
}
/**
* Enables profiling for the current request / CLI execution
*/
public function enable($flags = null, $options = null)
{
$this->running = false;
// 'REQUEST_TIME_FLOAT' isn't available before 5.4.0
// https://www.php.net/manual/en/reserved.variables.server.php
if (!isset($_SERVER['REQUEST_TIME_FLOAT'])) {
$_SERVER['REQUEST_TIME_FLOAT'] = microtime(true);
}
$profiler = $this->getProfiler();
if (!$profiler) {
throw new ProfilerException('Unable to create profiler: No suitable profiler found');
}
$saver = $this->getSaver();
if (!$saver) {
throw new ProfilerException('Unable to create saver');
}
if ($flags === null) {
$flags = $this->config['profiler.flags'];
}
if ($options === null) {
$options = $this->config['profiler.options'];
}
$profiler->enable($flags, $options);
$this->running = true;
}
/**
* Stop profiling. Return currently collected data
*
* @return array
*/
public function disable()
{
if (!$this->running) {
return array();
}
$profiler = $this->getProfiler();
if (!$profiler) {
// error for unable to create profiler already thrown in enable() method
// but this can also happen if methods are called out of sync
throw new ProfilerException('Unable to create profiler: No suitable profiler found');
}
$profile = new ProfilingData($this->config);
$this->running = false;
return $profile->getProfilingData($profiler->disable());
}
/**
* Saves collected profiling data
*
* @param array $data
*/
public function save(array $data = array())
{
if (!$data) {
return;
}
$saver = $this->getSaver();
if (!$saver) {
// error for unable to create saver already thrown in enable() method
// but this can also happen if methods are called out of sync
throw new ProfilerException('Unable to create profiler: Unable to create saver');
}
$saver->save($data);
}
/**
* Tells, if profiler is running or not
*
* @return bool
*/
public function isRunning()
{
return $this->running;
}
/**
* Returns value of `profiler.enable` function evaluation
*
* @return bool
*/
private function shouldRun()
{
$closure = $this->config['profiler.enable'];
return is_callable($closure) ? $closure() : false;
}
/**
* Calls register_shutdown_function .
* Registers this class' shutDown method as the shutdown handler
*
* @see Profiler::shutDown
*/
private function registerShutdownHandler()
{
// do not register shutdown function if the profiler isn't running
if (!$this->running) {
return;
}
register_shutdown_function(array($this, 'shutDown'));
}
/**
* @internal
*/
public function shutDown()
{
if ($this->flush) {
$this->flush();
}
try {
$this->stop();
} catch (ProfilerException $e) {
return;
}
}
private function flush()
{
// ignore_user_abort(true) allows your PHP script to continue executing, even if the user has terminated their request.
// Further Reading: http://blog.preinheimer.com/index.php?/archives/248-When-does-a-user-abort.html
// flush() asks PHP to send any data remaining in the output buffers. This is normally done when the script completes, but
// since we're delaying that a bit by dealing with the xhprof stuff, we'll do it now to avoid making the user wait.
ignore_user_abort(true);
if (function_exists('session_write_close')) {
session_write_close();
}
flush();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
}
/**
* @return ProfilerInterface|null
*/
private function getProfiler()
{
if ($this->profiler === null) {
$this->profiler = ProfilerFactory::create($this->config) ?: false;
}
return $this->profiler ?: null;
}
/**
* @return SaverInterface|null
*/
private function getSaver()
{
if ($this->saveHandler === null) {
$this->saveHandler = SaverFactory::create($this->config['save.handler'], $this->config) ?: false;
}
return $this->saveHandler ?: null;
}
/**
* @return array
*/
private function getDefaultConfig()
{
return array(
'save.handler' => Profiler::SAVER_STACK,
'save.handler.stack' => array(
'savers' => array(
Profiler::SAVER_UPLOAD,
Profiler::SAVER_FILE,
),
'saveAll' => false,
),
'save.handler.file' => array(
'filename' => sys_get_temp_dir() . '/xhgui.data.jsonl',
),
'profiler.enable' => function () {
return true;
},
'profiler.flags' => array(
ProfilingFlags::CPU,
ProfilingFlags::MEMORY,
ProfilingFlags::NO_BUILTINS,
ProfilingFlags::NO_SPANS,
),
'profiler.options' => array(),
'profiler.exclude-env' => array(),
'profiler.simple_url' => function ($url) {
return preg_replace('/=\d+/', '', $url);
},
'profiler.replace_url' => null,
);
}
}
<?php
namespace Xhgui\Profiler;
use Xhgui\Profiler\Exception\ProfilerException;
use Xhgui\Profiler\Profilers\ProfilerInterface;
final class ProfilerFactory
{
/**
* Creates Profiler instance that can be used.
*
* It returns first profiler, that is usable in testing them in this order:
* 2) tideways_xhprof
* 2) tideways
* 1) uprofiler
* 3) xhprof
*
* @return ProfilerInterface|null
*/
public static function create(array $config)
{
$adapters = array(
Profiler::PROFILER_TIDEWAYS_XHPROF => function () {
return new Profilers\TidewaysXHProf();
},
Profiler::PROFILER_TIDEWAYS => function () {
return new Profilers\Tideways();
},
Profiler::PROFILER_UPROFILER => function () {
return new Profilers\UProfiler();
},
Profiler::PROFILER_XHPROF => function () {
return new Profilers\XHProf();
},
);
if (isset($config['profiler'])) {
$profiler = $config['profiler'];
if (!isset($adapters[$profiler])) {
throw new ProfilerException("Specified profiler '$profiler' is not supported");
}
$adapters = array(
$profiler => $adapters[$profiler],
);
}
foreach ($adapters as $profiler => $factory) {
$adapter = $factory($config);
if ($adapter->isSupported()) {
return $adapter;
}
}
return null;
}
}
<?php
namespace Xhgui\Profiler\Profilers;
abstract class AbstractProfiler implements ProfilerInterface
{
/**
* Combines flags using "bitwise-OR".
*
* Map generic Profiler\ProfilingFlags to {SPECIFIC_PROFILER_NAME_HERE} implementation
*
* @param array $flags
* @param array $flagMap an array with the structure [generic_flag => specific_profiler_flag], e.g. [ProfilingFlags::CPU => XHPROF_FLAGS_CPU]
* @return int
*/
protected function combineFlags(array $flags, array $flagMap)
{
$combinedFlag = 0;
foreach ($flags as $flag) {
$mappedFlag = array_key_exists($flag, $flagMap) ? $flagMap[$flag] : $flag;
$combinedFlag |= $mappedFlag;
}
return $combinedFlag;
}
}
<?php
namespace Xhgui\Profiler\Profilers;
interface ProfilerInterface
{
/**
* @return bool
*/
public function isSupported();
/**
* Enable profiling.
*
* @param array $flags
* @param array $options
*/
public function enable($flags = array(), $options = array());
/**
* Disable (stop) the profiler. Return the collected data.
*
* @return array
*/
public function disable();
}
<?php
namespace Xhgui\Profiler\Profilers;
use Xhgui\Profiler\ProfilingFlags;
/**
* v4 (tideways)
*
* @see https://github.com/tideways/php-profiler-extension
*/
class Tideways extends AbstractProfiler
{
const EXTENSION_NAME = 'tideways';
public function isSupported()
{
return extension_loaded(self::EXTENSION_NAME);
}
public function enable($flags = array(), $options = array())
{
tideways_enable($this->combineFlags($flags, $this->getProfileFlagMap()), $options);
}
public function disable()
{
return tideways_disable();
}
private function getProfileFlagMap()
{
return array(
ProfilingFlags::CPU => TIDEWAYS_FLAGS_CPU,
ProfilingFlags::MEMORY => TIDEWAYS_FLAGS_MEMORY,
ProfilingFlags::NO_BUILTINS => TIDEWAYS_FLAGS_NO_BUILTINS,
ProfilingFlags::NO_SPANS => TIDEWAYS_FLAGS_NO_SPANS,
);
}
}
<?php
namespace Xhgui\Profiler\Profilers;
use Xhgui\Profiler\ProfilingFlags;
/**
* v5 (tideways_xhprof)
*
* @see https://github.com/tideways/php-profiler-extension
*/
class TidewaysXHProf extends AbstractProfiler
{
const EXTENSION_NAME = 'tideways_xhprof';
public function isSupported()
{
return extension_loaded(self::EXTENSION_NAME);
}
/**
* @see https://github.com/tideways/php-xhprof-extension#usage
*/
public function enable($flags = array(), $options = array())
{
tideways_xhprof_enable($this->combineFlags($flags, $this->getProfileFlagMap()));
}
public function disable()
{
return tideways_xhprof_disable();
}
private function getProfileFlagMap()
{
return array(
ProfilingFlags::CPU => TIDEWAYS_XHPROF_FLAGS_CPU,
ProfilingFlags::MEMORY => TIDEWAYS_XHPROF_FLAGS_MEMORY,
ProfilingFlags::NO_BUILTINS => TIDEWAYS_XHPROF_FLAGS_NO_BUILTINS,
ProfilingFlags::NO_SPANS => 0,
);
}
}
<?php
namespace Xhgui\Profiler\Profilers;
use Xhgui\Profiler\ProfilingFlags;
class UProfiler extends AbstractProfiler
{
const EXTENSION_NAME = 'uprofiler';
public function isSupported()
{
return extension_loaded(self::EXTENSION_NAME);
}
public function enable($flags = array(), $options = array())
{
uprofiler_enable($this->combineFlags($flags, $this->getProfileFlagMap()), $options);
}
public function disable()
{
return uprofiler_disable();
}
private function getProfileFlagMap()
{
return array(
ProfilingFlags::CPU => UPROFILER_FLAGS_CPU,
ProfilingFlags::MEMORY => UPROFILER_FLAGS_MEMORY,
ProfilingFlags::NO_BUILTINS => UPROFILER_FLAGS_NO_BUILTINS,
ProfilingFlags::NO_SPANS => 0,
);
}
}
<?php
namespace Xhgui\Profiler\Profilers;
use Xhgui\Profiler\ProfilingFlags;
class XHProf extends AbstractProfiler
{
const EXTENSION_NAME = 'xhprof';
public function isSupported()
{
return extension_loaded(self::EXTENSION_NAME);
}
/**
* @see https://www.php.net/manual/en/function.xhprof-enable.php
*/
public function enable($flags = array(), $options = array())
{
xhprof_enable($this->combineFlags($flags, $this->getProfileFlagMap()), $options);
}
public function disable()
{
return xhprof_disable();
}
/**
* @see https://www.php.net/manual/en/xhprof.constants.php
*/
private function getProfileFlagMap()
{
/*
* This is disabled on PHP 5.5+ as it causes a segfault
*
* @see https://github.com/perftools/xhgui-collector/commit/d1236d6422bfc42ac212befd0968036986885ccd
*/
$noBuiltins = PHP_MAJOR_VERSION === 5 && PHP_MINOR_VERSION > 4 ? 0 : XHPROF_FLAGS_NO_BUILTINS;
return array(
ProfilingFlags::CPU => XHPROF_FLAGS_CPU,
ProfilingFlags::MEMORY => XHPROF_FLAGS_MEMORY,
ProfilingFlags::NO_BUILTINS => $noBuiltins,
ProfilingFlags::NO_SPANS => 0,
);
}
}
<?php
namespace Xhgui\Profiler;
class ProfilingData
{
/** @var array */
private $excludeEnv;
/** @var callable|null */
private $simpleUrl;
/** @var callable|null */
private $replaceUrl;
public function __construct(array $config = array())
{
$this->excludeEnv = isset($config['profiler.exclude-env']) ? (array)$config['profiler.exclude-env'] : array();
$this->simpleUrl = isset($config['profiler.simple_url']) ? $config['profiler.simple_url'] : null;
$this->replaceUrl = isset($config['profiler.replace_url']) ? $config['profiler.replace_url'] : null;
}
/**
* @return array
*/
public function getProfilingData(array $profile)
{
$url = $this->getUrl();
$requestTimeFloat = explode('.', $_SERVER['REQUEST_TIME_FLOAT']);
if (!isset($requestTimeFloat[1])) {
$requestTimeFloat[1] = 0;
}
$allowedServerKeys = array(
'DOCUMENT_ROOT',
'HTTPS',
'HTTP_HOST',
'HTTP_USER_AGENT',
'PATH_INFO',
'PHP_AUTH_USER',
'PHP_SELF',
'QUERY_STRING',
'REMOTE_ADDR',
'REMOTE_USER',
'REQUEST_METHOD',
'REQUEST_TIME',
'REQUEST_TIME_FLOAT',
'SERVER_ADDR',
'SERVER_NAME',
'UNIQUE_ID',
);
$serverMeta = array_intersect_key($_SERVER, array_flip($allowedServerKeys));
$meta = array(
'url' => $url,
'get' => $_GET,
'env' => $this->getEnvironment($_ENV),
'SERVER' => $serverMeta,
'simple_url' => $this->getSimpleUrl($url),
'request_ts_micro' => array('sec' => $requestTimeFloat[0], 'usec' => $requestTimeFloat[1]),
// these are superfluous and should be dropped in the future
'request_ts' => array('sec' => $requestTimeFloat[0], 'usec' => 0),
'request_date' => date('Y-m-d', $requestTimeFloat[0]),
);
$data = array(
'profile' => $profile,
'meta' => $meta,
);
return $data;
}
/**
* @param array $env
* @return array
*/
private function getEnvironment(array $env)
{
foreach ($this->excludeEnv as $key) {
unset($env[$key]);
}
return $env;
}
/**
* Creates a simplified URL given a standard URL.
* Does the following transformations:
*
* - Remove numeric values after =.
*
* @param string $url
* @return string
*/
private function getSimpleUrl($url)
{
if (is_callable($this->simpleUrl)) {
return call_user_func($this->simpleUrl, $url);
}
return preg_replace('/=\d+/', '', $url);
}
/**
* @return string
*/
private function getUrl()
{
$url = array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : null;
if (!$url && isset($_SERVER['argv'])) {
$cmd = basename($_SERVER['argv'][0]);
$url = $cmd . ' ' . implode(' ', array_slice($_SERVER['argv'], 1));
}
if (is_callable($this->replaceUrl)) {
$url = call_user_func($this->replaceUrl, $url);
}
return $url;
}
}
<?php
namespace Xhgui\Profiler;
final class ProfilingFlags
{
const CPU = 'PROFILER_CPU_PROFILING';
const MEMORY = 'PROFILER_MEMORY_PROFILING';
const NO_BUILTINS = 'NO_BUILTINS';
const NO_SPANS = 'NO_SPANS';
}
<?php
namespace Xhgui\Profiler\Saver;
use Xhgui_Saver_Interface;
abstract class AbstractSaver implements SaverInterface
{
protected $saver;
public function __construct(Xhgui_Saver_Interface $saver)
{
$this->saver = $saver;
}
public function save(array $data)
{
return $this->saver->save($data);
}
}
<?php
namespace Xhgui\Profiler\Saver;
class FileSaver implements SaverInterface
{
/** @var string */
private $file;
public function __construct($file)
{
$this->file = $file;
}
public function isSupported()
{
return $this->file && is_writable(dirname($this->file));
}
public function save(array $data)
{
$json = json_encode($data);
return file_put_contents($this->file, $json . PHP_EOL, FILE_APPEND);
}
}
<?php
namespace Xhgui\Profiler\Saver;
use Xhgui_Saver_Mongo;
/**
* @property Xhgui_Saver_Mongo $saver
*/
class MongoSaver extends AbstractSaver
{
public function isSupported()
{
if (!$this->saver instanceof Xhgui_Saver_Mongo) {
return false;
}
return class_exists('MongoClient');
}
public function save(array $data)
{
if (isset($data['profile'])) {
$data['profile'] = $this->encodeProfile($data['profile']);
}
$result = parent::save($data);
return !empty($result);
}
/**
* MongoDB can't save keys with values containing a dot:
*
* InvalidArgumentException: invalid document for insert: keys cannot contain ".":
* "Zend_Controller_Dispatcher_Standard::loadClass==>load::controllers/ArticleController.php"
*
* Replace the dots with underscrore in keys.
*
* @link https://github.com/perftools/xhgui/issues/209
*/
private function encodeProfile(array $profile)
{
$results = array();
foreach ($profile as $k => $v) {
if (strpos($k, '.') !== false) {
$k = str_replace('.', '_', $k);
}
$results[$k] = $v;
}
return $results;
}
}
<?php
namespace Xhgui\Profiler\Saver;
use Xhgui_Saver_Pdo;
/**
* @property Xhgui_Saver_Pdo $saver
*/
class PdoSaver extends AbstractSaver
{
public function isSupported()
{
if (!$this->saver instanceof Xhgui_Saver_Pdo) {
return false;
}
return class_exists('PDO');
}
public function save(array $data)
{
parent::save($data);
return true;
}
}
<?php
namespace Xhgui\Profiler\Saver;
interface SaverInterface
{
/**
* @return bool
*/
public function isSupported();
/**
* @return bool
*/
public function save(array $data);
}
<?php
namespace Xhgui\Profiler\Saver;
use Exception;
/**
* Save to stack of savers.
*
* Supports saving to all savers, or to first successful one.
*/
class StackSaver implements SaverInterface
{
/** @var array */
private $savers;
/** @var bool */
private $saveAll;
public function __construct(array $savers, $saveAll = false)
{
$this->savers = $this->validateSavers($savers);
$this->saveAll = (bool)$saveAll;
}
public function isSupported()
{
return count($this->savers) > 0;
}
public function save(array $data)
{
$result = false;
foreach ($this->savers as $saver) {
try {
if (!$saver->save($data)) {
continue;
}
$result = true;
} catch (Exception $e) {
continue;
}
// if not save all, then break on first successful save
if (!$this->saveAll) {
break;
}
}
return $result;
}
private function validateSavers(array $savers)
{
$result = array();
foreach ($savers as $saver) {
try {
if (!$saver->isSupported()) {
continue;
}
} catch (Exception $e) {
continue;
}
$result[] = $saver;
}
return $result;
}
}
<?php
namespace Xhgui\Profiler\Saver;
use Xhgui\Profiler\Exception\ProfilerException;
class UploadSaver implements SaverInterface
{
/** @var string */
private $url;
/** @var int */
private $timeout;
public function __construct($url, $token, $timeout)
{
$this->url = $url;
if ($token) {
$this->url .= '?&token=' . $token;
}
$this->timeout = $timeout;
}
public function isSupported()
{
return $this->url && function_exists('curl_init');
}
public function save(array $data)
{
$json = json_encode($data);
$this->submit($this->url, $json);
return true;
}
/**
* @param string $url
* @param string $payload
*/
private function submit($url, $payload)
{
$ch = curl_init($url);
if (!$ch) {
throw new ProfilerException('Failed to create cURL resource');
}
$headers = array(
// Prefer to receive JSON back
'Accept: application/json',
// The sent data is JSON
'Content-Type: application/json',
);
$res = curl_setopt_array($ch, array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_FOLLOWLOCATION => 1,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => $this->timeout,
));
if (!$res) {
throw new ProfilerException('Failed to set cURL options');
}
$result = curl_exec($ch);
if ($result === false) {
throw new ProfilerException('Failed to submit data');
}
curl_close($ch);
$response = json_decode($result, true);
if (!$response) {
throw new ProfilerException('Failed to decode response');
}
if (isset($response['error']) && $response['error']) {
$message = isset($response['message']) ? $response['message'] : 'Error in response';
throw new ProfilerException($message);
}
}
}
<?php
namespace Xhgui\Profiler;
use Xhgui\Profiler\Exception\ProfilerException;
use Xhgui\Profiler\Saver\SaverInterface;
use Xhgui_Saver;
use Xhgui_Saver_Interface;
final class SaverFactory
{
/**
* @param string $saveHandler
* @param array $config
* @return SaverInterface|null
*/
public static function create($saveHandler, array $config = array())
{
switch ($saveHandler) {
case Profiler::SAVER_FILE:
$defaultConfig = array(
'filename' => null,
);
$userConfig = isset($config['save.handler.file']) && is_array($config['save.handler.file']) ? $config['save.handler.file'] : array();
$saverConfig = array_merge($defaultConfig, $userConfig);
$saver = new Saver\FileSaver($saverConfig['filename']);
break;
case Profiler::SAVER_UPLOAD:
$defaultConfig = array(
'uri' => null, // @deprecated
'url' => null,
'token' => null,
'timeout' => 3,
);
$userConfig = isset($config['save.handler.upload']) && is_array($config['save.handler.upload']) ? $config['save.handler.upload'] : array();
$saverConfig = array_merge($defaultConfig, $userConfig);
$saver = new Saver\UploadSaver($saverConfig['url'] ?: $saverConfig['uri'], $saverConfig['token'], $saverConfig['timeout']);
break;
case Profiler::SAVER_STACK:
$defaultConfig = array(
'savers' => array(),
'saveAll' => false,
);
$userConfig = isset($config['save.handler.stack']) && is_array($config['save.handler.stack']) ? $config['save.handler.stack'] : array();
$saverConfig = array_merge($defaultConfig, $userConfig);
$savers = array();
foreach ($saverConfig['savers'] as $saver) {
$instance = self::create($saver, $config);
if ($instance) {
$savers[] = $instance;
}
}
$saver = new Saver\StackSaver($savers, $saverConfig['saveAll']);
break;
default:
// create via xhgui-collector
if (!class_exists('\Xhgui_Saver')) {
throw new ProfilerException("For {$saveHandler} you need to install xhgui-collector package: composer require perftools/xhgui-collector");
}
$config = self::migrateConfig($config, $saveHandler);
$legacySaver = Xhgui_Saver::factory($config);
$saver = static::getAdapter($legacySaver);
break;
}
if (!$saver || !$saver->isSupported()) {
return null;
}
return $saver;
}
/**
* Prepare config for Xhgui_Saver specific to $saveHandler
*
* @param array $config
* @param string $saveHandler
* @return array
*/
private static function migrateConfig(array $config, $saveHandler)
{
switch ($saveHandler) {
case Profiler::SAVER_MONGODB:
if (isset($config['save.handler.mongodb']['dsn'])) {
$config['db.host'] = $config['save.handler.mongodb']['dsn'];
}
if (isset($config['save.handler.mongodb']['database'])) {
$config['db.db'] = $config['save.handler.mongodb']['database'];
}
if (isset($config['save.handler.mongodb']['options'])) {
$config['db.options'] = $config['save.handler.mongodb']['options'];
} else {
$config['db.options'] = array();
}
if (isset($config['save.handler.mongodb']['driverOptions'])) {
$config['db.driverOptions'] = $config['save.handler.mongodb']['driverOptions'];
} else {
$config['db.driverOptions'] = array();
}
break;
case Profiler::SAVER_PDO:
if (isset($config['save.handler.pdo'])) {
$config['pdo'] = $config['save.handler.pdo'];
}
break;
}
$config['save.handler'] = $saveHandler;
return $config;
}
private static function getAdapter(Xhgui_Saver_Interface $saver)
{
$adapters = array(
new Saver\PdoSaver($saver),
new Saver\MongoSaver($saver),
);
$available = array_filter($adapters, function (SaverInterface $adapter) {
return $adapter->isSupported();
});
return current($available) ?: null;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment