热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

干货:构建复杂的Eloquent搜索过滤

文章转发自专业的Laravel开发者社区,原始链接:https:learnku.comlaravelt…最近,我需要在开发的事件管理系统中实现搜索功能。一开始只是简单的几个选项(通

《干货:构建复杂的 Eloquent 搜索过滤》

文章转发自专业的Laravel开发者社区,原始链接:
https://learnku.com/laravel/t…

最近,我需要在开发的事件管理系统中实现搜索功能。 一开始只是简单的几个选项 (通过名称,邮箱等搜索),到后面参数变得越来越多。

今天,我会介绍整个过程以及如何构建灵活且可扩展的搜索系统。如果你想查看代码,请访问 Git 仓库 。

我们将创造什么

我们公司需要一种跟踪我们与世界各地客户举办的各种活动和会议的方式。我们目前的唯一方法是让每位员工在 Outlook 日程表上存储会议的详细信息。可拓展性较差!

我们需要公司的每个人都可以访问,来查看我们客户的被邀请的详细信息以及他们的RSVP(国际缩用语:请回复)状态。

这样,我们可以通过上次与他们互动的数据来确定哪些用户可以邀请来参加未来的活动。

《干货:构建复杂的 Eloquent 搜索过滤》

使用高级搜索过滤器查找的截图

查找用户

常用过滤用户的方法:

  • 通过姓名,电子邮件,位置
  • 通过用户工作的公司
  • 被邀请参加特定活动的用户
  • 参加过特定活动的用户
  • 邀请及已参加活动的用户
  • 邀请但尚未回复的用户
  • 答应参加但未出席的用户
  • 分配给销售经理的用户

虽然这个列表不算完整,但可以让我们知道需要多少个过滤器。这将是个挑战!

《干货:构建复杂的 Eloquent 搜索过滤》

前端的条件过滤的截图。

模型及模型关联

在这个例子中我们回用到很多模型:

  • User —  代表被邀请参加活动的用户。一个用户可以参加很多活动。
  • Event — 代表我公司举办的活动。活动可以有多个。
  • Rsvp —  代表用户对活动邀请的回复。一个用户对一个活动的回复是一对一的。
  • Manager —  一个用户可以对应多个我公司的销售经理.

搜索的需求

在开始代码之前,我想先把搜索的需求明确一下。也就是说我要很清楚地知道我要对哪些数据做搜索功能。

下面就是一个例子:

{
"name": "Billy",
"company": "Google",
"city": "London",
"event": "key-note-presentation-new-york-05-2016",
"responded": true,
"response": "I will be attending",
"managers": [
"Tom Jones",
"Joe Bloggs"
],
}

总结一下上面数据想表达的搜索的条件:

客人的名字是 ‘Billy’,来自 ‘Google’ 公司,目前居住在 ‘London’,已经对 ‘key-note-presentation-new-york-05–2016’ 的活动邀请做出了回复,并且回复的内容是 ‘I will be attending’,负责跟进这位客人的销售经理是 ‘Tom Jones’ 或者 ‘Joe Bloggs’。

开始 — 最佳实践

我是一个坚定不移的极简主义者,我坚信少即是多。下面就让我们以最简单的方式探索出解决这个需求的最佳实践。

首先,在  routes.php 文件中添加如下代码:

Route::post('/search', 'SearchController@filter');

接下来,创建  SearchController.

php artisan make:controller SearchController

添加前面路由中明确的 filter()  方法:

namespace App\Http\Controllers;
use App\User;
use App\Http\Requests;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class SearchController extends Controller
{
public function filter(Request $request, User $user)
{
//
}
}

由于我们需要在 filter 方法中处理请求提交的数据,所以我把 Request 类做了依赖注入。Laravel 的服务容器 会解析这个依赖,我们可以在方法中直接使用 Request 的实例,也就是 $request。User 类也是同样道理,我们需要从中检索一些数据。

这个搜索需求有一点比较麻烦的是,每个参数都是可选的。所以我们要先写一系列的条件语句来判断每个参数是否存在:

这是我初步写出来的代码:

public function filter(Request $request, User $user)
{
// 根据姓名查找用户
if ($request->has('name')) {
return $user->where('name', $request->input('name'))->get();
}
// 根据公司名查找用户
if ($request->has('company')) {
return $user->where('company', $request->input('company'))
->get();
}
// 根据城市查找用户
if ($request->has('city')) {
return $user->where('city', $request->input('city'))->get();
}
// 继续根据其他条件查找
// 再无其他条件,
// 返回所有符合条件的用户。
// 在实际项目中需要做分页处理。
return User::all();
}

很明显,上面的代码逻辑是错误的。

首先,它只会根据一个条件去检索用户表,然后就返回了。所以,通过上面的代码逻辑,我们根本无法获得姓名为 ‘Billy’, 而且住在 ‘London’ 的用户。

实现这种目的的一种方式是嵌套条件:

// 根据用户名搜索用户
if ($request->has('name')) {
// 是否还提供了 'city' 搜索参数
if ($request->has('city')) {
// 基于用户名及城市搜索用户
return $user->where('name', $request->input('name'))
->where('city', $request->input('city'))
->get();
}
return $user->where('name', $request->input('name'))->get();
}

我确信你可以看到这在两个或者三个参数的时候起作用,但是一旦我们添加更多选项,这将会难以管理。

改进我们的搜索 api

所以我们如何让这个生效,而同时不会因为嵌套条件而变得疯狂?

我们可以使用 User 模型继续重构,来使用 builder 而不是直接返回模型。

public function filter(Request $request, User $user)
{
$user = $user->newQuery();
// 根据用户名搜索用户
if ($request->has('name')) {
$user->where('name', $request->input('name'));
}
// 根据用户公司信息搜索用户
if ($request->has('company')) {
$user->where('company', $request->input('company'));
}
// 根据用户城市信息搜索用户
if ($request->has('city')) {
$user->where('city', $request->input('city'));
}
// 继续执行其他过滤
// 获得并返回结果
return $user->get();
}

好多了!我们现在可以将每个搜索参数做为修饰符添加到从  $user->newQuery() 返回的查询实例中。

我们现在可以根据所有的参数来做搜索了, 再多参数都不怕.

一起来实践吧:

$user = $user->newQuery();
// 根据姓名查找用户
if ($request->has('name')) {
$user->where('name', $request->input('name'));
}
// 根据公司名查找用户
if ($request->has('company')) {
$user->where('company', $request->input('company'));
}
// 根据城市查找用户
if ($request->has('city')) {
$user->where('city', $request->input('city'));
}
// 只查找有对接我公司销售经理的用户
if ($request->has('managers')) {
$user->whereHas('managers', function ($query) use ($request) {
$query->whereIn('managers.name', $request->input('managers'));
});
}
// 如果有 'event' 参数
if ($request->has('event')) {
// 只查找被邀请的用户
$user->whereHas('rsvp.event', function ($query) use ($request) {
$query->where('event.slug', $request->input('event'));
}); // 只查找回复邀请的用户( 以任何形式回复都可以 )
if ($request->has('responded')) {
$user->whereHas('rsvp', function ($query) use ($request) {
$query->whereNotNull('responded_at');
});
}
// 只查找回复邀请的用户( 限制回复的具体内容 )
if ($request->has('response')) {
$user->whereHas('rsvp', function ($query) use ($request) {
$query->where('response', 'I will be attending');
});
}
}
// 最终获取对象并返回
return $user->get();

搞定,棒极了!

是否还需要重构?

通过上面的代码我们实现了业务需求,可以根据搜索条件返回正确的用户信息。但是我们能说这是最佳实践吗?显然是不能。

现在是通过一系列条件判断的嵌套来实现业务逻辑,而且所有的逻辑都在控制器里,这样做真的合适吗?

这可能是一个见仁见智的问题,最好还是结合自己的项目,具体问题具体分析。如果你的项目比较小,逻辑相对简单,而且只是一个短期需求的项目,那么就不必纠结这个问题了,直接照上面的逻辑写就好了。 

然而,如果你是在构建一个比较复杂的项目,那么我们还是需要更加优雅且扩展性好的解决方案。

编写新的搜索 api

当我要写一个功能接口的时候,我不会立刻去写核心代码,我通常会先想想我要怎么用这个接口。这可能就是俗称的「面向结果编程」(或者说是「结果导向思维」)。

「在你写一个组件之前,建议你先写一些要用这个组件的测试代码。通过这种方式,你会更加清晰地知道你究竟要写哪些函数,以及传哪些必要的参数,这样你才能写出真正好用的接口。

因为写接口的目的是简化使用组件的代码,而不是简化接口自身的代码。」 ( 摘自: c2.com)

根据我的经验,这个方法能帮助我写出可读性更强,更加优雅的程序。还有一个很大的额外收获就是,通过这种阶段性的验收测试,我能更好地抓住商业需求。因此,我可以很自信地说我写的程序可以很好地满足市场的需求,具有很高商业价值。

以下添加到搜索功能的代码中,我希望我的搜索 api 是这样写的:

return UserSearch::apply($filters);

这样有着很好的可读性。 根据经验, 如果我查阅代码能想看文章的句子一样,那会非常美妙。像刚刚的情况下:

搜索用户时加上一个过滤器再返回搜索结果。

这对技术人员和非技术人员都有意义。

我想我需要新建一个 UserSearch 类,还需要一个静态的 apply 函数来接收过滤条件。让我开始吧:

namespace App\Search;
use Illuminate\Http\Request;
class UserSearch
{
public static function apply(Request $filters)
{
// 返回搜索结果
}
}

最简单的方式,让我们把控制器中的代码复制到 apply 函数中:

namespace App\UserSearch;
use App\User;
use Illuminate\Http\Request;
class UserSearch
{
public static function apply(Request $filters)
{
$user = (new User)->newQuery();
// 基于用户名搜索
if ($filters->has('name')) {
$user->where('name', $filters->input('name'));
}
// 基于用户的公司名搜索
if ($filters->has('company')) {
$user->where('company', $filters->input('company'));
}
// 基于用户的城市名搜索
if ($filters->has('city')) {
$user->where('city', $filters->input('city'));
}
// 只返回分配了销售经理的用户
if ($filters->has('managers')) {
$user->whereHas('managers',
function ($query) use ($filters) {
$query->whereIn('managers.name',
$filters->input('managers'));
});
}
// 搜索条件中是否包含 'event’ ?
if ($filters->has('event')) {
// 只返回被邀请参加了活动的用户
$user->whereHas('rsvp.event',
function ($query) use ($filters) {
$query->where('event.slug',
$filters->input('event'));
});

// 只返回以任何形式答复了邀请的用户
if ($filters->has('responded')) {
$user->whereHas('rsvp',
function ($query) use ($filters) {
$query->whereNotNull('responded_at');
});
}

// 只返回以某种方式答复了邀请的用户
if ($filters->has('response')) {
$user->whereHas('rsvp',
function ($query) use ($filters) {
$query->where('response',
'I will be attending');
});
}
}
// 返回搜索结果
return $user->get();
}
}

我们做了一系列的改变。 首先, 我们将在控制器中的 $request 变量更名为 filters 来提高可读性。

其次,由于 newQuery() 方法不是静态方法,无法通过 User 类静态调用,所以我们需要先创建一个 User 对象,再调用这个方法:

$user = (new User)->newQuery();

调用上面的 UserSearch 接口,对控制器的代码进行重构:

namespace App\Http\Controllers;
use App\Http\Requests;
use App\Search\UserSearch;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class SearchController extends Controller
{
public function filter(Request $request)
{
return UserSearch::apply($request);
}
}

好多了,是不是?把一系列的条件判断交给专门的类处理,使控制器的代码简介清新。

下面进入见证奇迹的时刻

在这篇文章的例子中,一共有 7 个过滤条件,但是现实的情况是更多更多。所以在这种情况下,只用一个文件来处理所有的过滤逻辑,就显得差强人意了。扩展性不好,而且也不符合 S.O.L.I.D. principles 原则。目前,apply()  方法需要处理这些逻辑:

  • 检查参数是否存在
  • 把参数转成查询条件
  • 执行查询

如果我想增加一个新的过滤条件,或者修改一下现有的某个过滤条件的逻辑,我都要不停地修改 UserSearch 类,因为所有过滤条件的处理都在这一个类里,随着业务逻辑的增加,会有点尾大不掉的感觉。所以对每个过滤条件单独建个类文件是非常有必要的。

先从 Name 条件开始吧。但是,就像我们前面讲的,还是想一下我们需要怎样使用这种单一条件过滤的接口。

我希望可以这样调用这个接口:

$user = (new User)->newQuery();
$user = static::applyFiltersToQuery($filters, $user);
return $user->get();

不过这里再使用 $user 这个变量名就不合适了,应该用 $query 更有意义。

public static function apply(Request $filters)
{
$query = (new User)->newQuery();
$query = static::applyFiltersToQuery($filters, $query);
return $query->get();
}

然后把所有条件过滤的逻辑都放到 applyFiltersToQuery() 这个新接口里。

下面开始创建第一个条件过滤类:Name.

namespace App\UserSearch\Filters;
class Name
{
public static function apply($builder, $value)
{
return $builder->where('name', $value);
}
}

在这个类里定义一个静态方法 apply(),这个方法接收两个参数,一个是 Builder 实例,另一个是过滤条件的值( 在这个例子中,这个值是 ‘Billy’ )。然后带着这个过滤条件返回一个新的 Builder 实例。

接下来是 City 类:

namespace App\UserSearch\Filters;
class City
{
public static function apply($builder, $value)
{
return $builder->where('city', $value);
}
}

如你所见,City 类的代码逻辑跟 Name 类相同,只是过滤条件变成了 ‘city’。让每个条件过滤类都只有一个简单的 apply() 方法,而且方法接收的参数和处理的逻辑都相同,我们可以把这看成一个协议,这一点很重要,下面我会具体说明。

为了确保每个条件过滤类都能遵循这个协议,我决定写一个接口,让每个类都实现这个接口。

namespace App\UserSearch\Filters;
use Illuminate\Database\Eloquent\Builder;
interface Filter
{
/**
* 把过滤条件附加到 builder 的实例上
*
* @param Builder $builder
* @param mixed $value
* @return Builder $builder
*/
public static function apply(Builder $builder, $value);
}

我为这个接口的方法写了详细的注释,这样做的好处是,对于每一个实现这个接口的类,我都可以利用我的 IDE ( PHPStorm ) 自动生成同样的注释。

下面,分别在 Name 和 City 类中实现这个 Filter 接口:

namespace App\UserSearch\Filters;
use Illuminate\Database\Eloquent\Builder;
class Name implements Filter
{
/**
* 把过滤条件附加到 builder 的实例上
*
* @param Builder $builder
* @param mixed $value
* @return Builder $builder
*/
public static function apply(Builder $builder, $value)
{
return $builder->where('name', $value);
}
}

以及

namespace App\UserSearch\Filters;
use Illuminate\Database\Eloquent\Builder;
class City implements Filter
{
/**
* 把过滤条件附加到 builder 的实例上
*
* @param Builder $builder
* @param mixed $value
* @return Builder $builder
*/
public static function apply(Builder $builder, $value)
{
return $builder->where('city', $value);
}
}

完美。现在已经有两个条件过滤类完美地遵循了这个协议。把我的目录结构附在下面给大家参考一下:

《干货:构建复杂的 Eloquent 搜索过滤》

这是到目前为止关于搜索的文件结构。

我把所有的条件过滤类的文件放在一个单独的文件夹里,这让我对已有的过滤条件一目了然。

使用新的过滤器

现在回过头来看 UserSearch 类的 applyFiltersToQuery() 方法,发现我们可以再做一些优化了。

首先,把每个条件判断里构建查询语句的工作,交给对应的过滤类去做。

// 根据姓名搜索用户
if ($filters->has('name')) {
$query = Name::apply($query, $filters->input('name'));
}
// 根据城市搜索用户
if ($filters->has('city')) {
$query = City::apply($query, $filters->input('city'));
}

现在根据过滤条件构建查询语句的工作已经转给各个相应的过滤类了,但是判断每个过滤条件是否存在的工作,还是通过一系列的条件判断语句完成的。而且条件判断的参数都是写死的,一个参数对应一个过滤类。这样我每增加一个新的过滤条件,我都要重新修改 UserSearch 类的代码。这显然是一个需要解决的问题。

其实,根据我们前面介绍的命名规则, 我们很容易把这段条件判断的代码改成动态的:

AppUserSearchFiltersName

AppUserSearchFiltersCity

就是结合命名空间和过滤条件的名称,动态地创建过滤类(当然,要对接收到的过滤条件参数做适当的处理)。

大概就是这个思路,下面是具体实现:

private static function applyFiltersToQuery(
Request $filters, Builder $query) {
foreach ($filters->all() as $filterName => $value) {
$decorator =
__NAMESPACE__ . '\\Filters\\' .
str_replace(' ', '', ucwords(
str_replace('_', ' ', $filterName)));
if (class_exists($decorator)) {
$query = $decorator::apply($query, $value);
}
}
return $query;
}

下面逐行分析这段代码:

foreach ($filters->all() as $filterName => $value) {

遍历所有的过滤参数,把参数名(比如 city)赋值给变量 $filterName,参数值(比如 London)复制给变量 $value

$decorator =
__NAMESPACE__ . '\\Filters\\' .
str_replace(' ', '', ucwords(
str_replace('_', ' ', $filterName)));

这里是对参数名进行处理,将下划线改成空格,让每个单词都首字母大写,然后去掉空格,如下例子:

"name" => App\UserSearch\Filters\Name,\
"company" => App\UserSearch\Filters\Company,\
"city" => App\UserSearch\Filters\City,\
"event" => App\UserSearch\Filters\Event,\
"responded" => App\UserSearch\Filters\Responded,\
"response" => App\UserSearch\Filters\Response,\
"managers" => App\UserSearch\Filters\Managers

如果有参数名是带下划线的,比如 has_responded,根据上面的规则,它将被处理成 HasResponded,因此,其相应的过滤类的名字也要是这个。

if (class_exists($decorator)) {

这里就是要先确定这个过滤类是存在的,再执行下面的操作,否则在客户端报错就尴尬了。

$query = $decorator::apply($query, $value);

这里就是神奇的地方了,PHP 允许把变量 $decorator 作为类,并调用其方法(在这里就是 apply() 方法了)。现在再看这个接口的代码,发现我们再次实力证明了磨刀不误砍柴工。现在我们可以确保每个过滤类对外响应一致,内部又可以分别处理各自的逻辑。

最后的优化

现在 UserSearch 类的代码应该已经比之前好多了,但是,我觉得还可以更好,所以我又做了些改动,这是最终版本:

namespace App\UserSearch;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;
class UserSearch
{
public static function apply(Request $filters)
{
$query =
static::applyDecoratorsFromRequest(
$filters, (new User)->newQuery()
);
return static::getResults($query);
} private static function applyDecoratorsFromRequest(Request $request, Builder $query)
{
foreach ($request->all() as $filterName => $value) {
$decorator = static::createFilterDecorator($filterName);
if (static::isValidDecorator($decorator)) {
$query = $decorator::apply($query, $value);
}
}
return $query;
} private static function createFilterDecorator($name)
{
return return __NAMESPACE__ . '\\Filters\\' .
str_replace(' ', '',
ucwords(str_replace('_', ' ', $name)));
} private static function isValidDecorator($decorator)
{
return class_exists($decorator);
}
private static function getResults(Builder $query)
{
return $query->get();
}
}

我最后决定去掉 applyFiltersToQuery() 方法,是因为感觉跟接口的主要方法名 apply() 有点冲突了。

而且,为了贯彻执行单一职责原则,我把原来 applyFiltersToQuery() 方法里比较复杂的逻辑又做了拆分,为动态创建过滤类名称,和确认过滤类是否存在的判断,都写了单独的方法。

这样,即便要扩展搜索接口,我也不需要再去反复修改 UserSearch 类里的代码了。需要增加新的过滤条件吗?简单,只要在 App\UserSearch\Filters 目录下创建一个过滤类,并使之实现 Filter 接口就 OK 了。

结论

我们已经把一个拥有所有搜索逻辑的巨大控制器方法保存成一个允许打开过滤器的模块化过滤系统,而不需要添加修改核心代码。 像评论里 @rockroxx所建议的,另一个重构的方案是把所有方法提取到 trait 并将 User  设置成  const  然后由 Interface 实现。

class UserSearch implements Searchable {
const MODEL = App\User;
use SearchableTrait;
}

如果你很好的理解了这个设计模式,你可以 利用多态代替多条件。

代码会提交到 GitHub 你可以 fork,测试和实验。

如何解决多条件高级搜索,我希望你能留下你的想法、建议和评论。


推荐阅读
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的详细步骤
    本文详细介绍了搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的步骤,包括环境说明、相关软件下载的地址以及所需的插件下载地址。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • 本文讨论了如何在codeigniter中识别来自angularjs的请求,并提供了两种方法的代码示例。作者尝试了$this->input->is_ajax_request()和自定义函数is_ajax(),但都没有成功。最后,作者展示了一个ajax请求的示例代码。 ... [详细]
  • MVC设计模式的介绍和演化过程
    本文介绍了MVC设计模式的基本概念和原理,以及在实际项目中的演化过程。通过分离视图、模型和控制器,实现了代码的解耦和重用,提高了项目的可维护性和可扩展性。详细讲解了分离视图、分离模型和分离控制器的具体步骤和规则,以及它们在项目中的应用。同时,还介绍了基础模型的封装和控制器的命名规则。该文章适合对MVC设计模式感兴趣的读者阅读和学习。 ... [详细]
  • Todayatworksomeonetriedtoconvincemethat:今天在工作中有人试图说服我:{$obj->getTableInfo()}isfine ... [详细]
  • 本文介绍了Foundation框架中一些常用的结构体和类,包括表示范围作用的NSRange结构体的创建方式,处理几何图形的数据类型NSPoint和NSSize,以及由点和大小复合而成的矩形数据类型NSRect。同时还介绍了创建这些数据类型的方法,以及字符串类NSString的使用方法。 ... [详细]
  • 本文介绍了在无法联网的情况下,通过下载rpm包离线安装zip和unzip的方法。详细介绍了如何搜索并下载合适的rpm包,以及如何使用rpm命令进行安装。 ... [详细]
author-avatar
vegg巛iegbaby
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有