williarin/wordpress-interop

与 WordPress 数据库协同工作的互操作性库

1.16.1 2024-07-26 11:22 UTC

README

Github Workflow

简介

此库旨在通过第三方应用程序简化与 WordPress 数据库的交互。它依赖于 Doctrine DBAL,看起来像 Doctrine ORM。

它可以执行一些简单任务,例如查询帖子、检索附件数据等。

您可以通过添加自己的存储库和查询方法来扩展它。

警告! 虽然它看起来像是一个 ORM,但它不是一个 ORM 库。它没有双向数据操作功能。将其视为一个简单的 WordPress 数据库操作辅助库。

安装

此库可以作为独立库使用

composer require williarin/wordpress-interop

或与 Symfony 一起使用

composer require williarin/wordpress-interop-bundle

专用存储库页面上找到 Symfony 扩展的文档。

使用方法

概述

$post = $manager->getRepository(Post::class)->find(15);

详细说明

首先,需要创建一个与您的 DBAL 连接相关联的实体管理器,目标为您的 WordPress 数据库。

$connection = DriverManager::getConnection(['url' => 'mysql://user:pass@localhost:3306/wp_mywebsite?serverVersion=8.0']);

$objectNormalizer = new ObjectNormalizer(
    new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())),
    new CamelCaseToSnakeCaseNameConverter(),
    null,
    new ReflectionExtractor()
);

$serializer = new Serializer([
    new DateTimeNormalizer(),
    new ArrayDenormalizer(),
    new SerializedArrayDenormalizer($objectNormalizer),
    $objectNormalizer,
]);

$manager = new EntityManager($connection, $serializer);

然后您可以查询数据库

/** @var PostRepository $postRepository */
$postRepository = $manager->getRepository(Post::class);
$myPost = $postRepository->find(15);
$allPosts = $postRepository->findAll();

文档

基本查询

这适用于从 BaseEntity 继承的任何实体。内置实体包括 PostPageAttachmentProduct,但您可以 创建自己的

// Fetch a post by ID
$post = $manager->getRepository(Post::class)->find(1);

// Fetch the latest published post
$post = $manager->getRepository(Post::class)
    ->findOneByPostStatus('publish', ['post_date' => 'DESC']);

// Fetch the latest published post which has 1 comment
$post = $manager->getRepository(Post::class)
    ->findOneBy(
        ['post_status' => 'publish', 'comment_count' => 1],
        ['post_date' => 'DESC'],
    );

// Fetch the latest published post which has the most comments
$post = $manager->getRepository(Post::class)
    ->findOneByPostStatus(
        'publish',
        ['comment_count' => 'DESC', 'post_date' => 'DESC'],
    );

// Fetch all posts which have draft or private status
$posts = $manager->getRepository(Post::class)
    ->findByPostStatus(new Operand(['draft', 'private'], Operand::OPERATOR_IN));

// Fetch all posts
$posts = $manager->getRepository(Post::class)->findAll();

// Fetch all private posts
$posts = $manager->getRepository(Post::class)->findByPostStatus('private');

// Fetch all products whose titles match regexp
$products = $manager->getRepository(Product::class)
    ->findByPostTitle(new Operand('Hoodie.*Pocket|Zipper', Operand::OPERATOR_REGEXP));

EAV 查询

EAV 术语指的是 WordPress 通过“元”术语(如 wp_postmetawp_termmetawp_usermeta 等)使用的 实体-属性-值模型。这里我们谈论的是 wp_postmeta

查询系统支持直接查询 EAV 属性。

在下面的示例中,skustock_statuswp_postmeta 表的属性。

注意:字段名映射到它们的属性名。例如,_sku 变为 sku,或 _wc_average_rating 变为 average_rating

// Fetch a product by its SKU
$product = $manager->getRepository(Product::class)->findOneBySku('woo-vneck-tee');

// Fetch the latest published product which is in stock
$product = $manager->getRepository(Product::class)
    ->findOneBy(
        ['stock_status' => 'instock', 'post_status' => 'publish'],
        ['post_date' => 'DESC'],
    );
    
// Fetch all published products which are in stock
$products = $manager->getRepository(Product::class)
    ->findBy(
        ['stock_status' => 'instock', 'post_status' => 'publish'],
        ['post_date' => 'DESC'],
    );

// Fetch all products whose sku match regexp
$products = $manager->getRepository(Product::class)
    ->findBySku(new Operand('hoodie.*logo|zipper', Operand::OPERATOR_REGEXP));

如果您查询了一个实体中不存在的 EAV 属性,将抛出 InvalidFieldNameException 异常。

为了允许查询额外的动态属性,在查询之前将 allow_extra_properties 选项设置为 true。但请注意,这些选项适用于存储库而不是查询,这意味着它们将适用于所有后续查询。

$page = $manager->getRepository(Page::class)
    ->setOptions([
        'allow_extra_properties' => true,
    ])
    ->findOneBy([
        new SelectColumns(['id', select_from_eav('wp_page_template')]),
        'post_status' => 'publish',
        'wp_page_template' => 'default',
    ])
;
// $page->wpPageTemplate === 'default'

嵌套条件

对于更复杂的查询需求,您可以添加嵌套条件。

注意:它仅适用于列,而不是 EAV 属性。

// Fetch Hoodies as well as products with at least 30 comments, all of which are in stock
$products = $manager->getRepository(Product::class)
    ->findBy([
        new NestedCondition(NestedCondition::OPERATOR_OR, [
            'post_title' => new Operand('Hoodie%', Operand::OPERATOR_LIKE),
            'comment_count' => new Operand(30, Operand::OPERATOR_GREATER_THAN_OR_EQUAL),
        ]),
        'stock_status' => 'instock',
    ]);

// Fetch two products by their SKU and two by their ID
$products = $manager->getRepository(Product::class)
    ->findBy([
        new NestedCondition(NestedCondition::OPERATOR_OR, [
            'sku' => new Operand(['woo-tshirt', 'woo-single'], Operand::OPERATOR_IN),
            'id' => new Operand([19, 20], Operand::OPERATOR_IN),
        ]),
    ]);
// count($products) === 4

EAV 关系条件

根据实体的 EAV 关系查询实体。

注意:EAV 字段必须使用其原始名称,这与直接 EAV 查询的映射字段不同。

// Fetch the featured image of the post with ID "4"
$attachment = $manager->getRepository(Attachment::class)
    ->findOneBy([
        new RelationshipCondition(4, '_thumbnail_id'),
    ]);

// Get featured images of posts 4, 13, 18 and 23 at once
$attachments = $manager->getRepository(Attachment::class)
    ->findBy([
        new RelationshipCondition(
            new Operand([4, 13, 18, 23], Operand::OPERATOR_IN),
            '_thumbnail_id',
        ),
    ]);

// Same as above example but include the original ID in the result
$attachments = $manager->getRepository(Attachment::class)
    ->findBy([
        new RelationshipCondition(
            new Operand([4, 13, 18, 23], Operand::OPERATOR_IN),
            '_thumbnail_id',
            'original_post_id',
        ),
    ]);
// $attachments[0]->originalPostId === 4

术语和分类关系条件

根据实体的术语和分类关系查询实体。

// Fetch products in the category "Hoodies"
$products = $manager->getRepository(Product::class)
    ->findBy([
        new TermRelationshipCondition([
            'taxonomy' => 'product_cat',
            'name' => 'Hoodies',
        ]),
    ]);

此外,您还可以从联合实体查询术语,并指定术语表的名称。

在这个例子中,我们假设产品有一个 related_product postmeta。

// Fetch a product's category and the category of its related product
$product = $manager->getRepository(Product::class)
    ->findOneBy([
        new SelectColumns([
            'id',
            'main.name AS category',
            'related.name AS related_category',
            select_from_eav(
                fieldName: 'related_product',
                metaKey: 'related_product', // needed as it's not starting with an underscore
            ),
        ]),
        new TermRelationshipCondition(
            ['taxonomy' => 'product_cat'],
            termTableAlias: 'main',
        ),
        new TermRelationshipCondition(
            ['taxonomy' => 'product_cat'],
            joinConditionField: 'related_product',
            termTableAlias: 'related',
        ),
        'id' => 22,
    ]);
// $product->category === 'Hoodies'
// $product->relatedCategory === 'Accessories'

如果没有指定,术语表别名默认为 t_0t_1 等。

还提供了一个特殊运算符 Operand::OPERATOR_IN_ALL 来匹配数组中的所有值。

// Fetch products that have both 'featured' and 'accessories' terms
$products = $manager->getRepository(Product::class)
    ->findBy([
        new TermRelationshipCondition([
            'slug' => new Operand(['featured', 'accessories'], Operand::OPERATOR_IN_ALL),
        ]),
    ]);

此运算符不仅限于术语查询,但它是最明显的用例。

帖子关系条件

根据其帖子关系查询术语。

// Fetch all terms of the product with SKU "super-forces-hoodie"
// belonging to all taxonomies except "product_tag", "product_type", "product_visibility".
$terms = $manager->getRepository(Term::class)
    ->findBy([
        new SelectColumns(['taxonomy', 'name']),
        new PostRelationshipCondition(Product::class, [
            'post_status' => new Operand(['publish', 'private'], Operand::OPERATOR_IN),
            'sku' => 'super-forces-hoodie',
        ]),
        'taxonomy' => new Operand(
            ['product_tag', 'product_type', 'product_visibility'],
            Operand::OPERATOR_NOT_IN,
        ),
    ]);

限制所选列

一次性查询所有列会变慢,尤其是当你有大量实体需要检索时。你可以像以下示例中一样限制查询的列。

它适用于基本列以及EAV属性。

// Fetch only products title and SKU
$products = $manager->getRepository(Product::class)
    ->findBy([
        new SelectColumns(['post_title', 'sku']),
        'sku' => new Operand('hoodie.*logo|zipper', Operand::OPERATOR_REGEXP),
    ]);

// Product entities are filled with null values except $postTitle and $sku

你还可以选择一个在你的实体中没有映射属性的字段。

$product = $manager->getRepository(Product::class)
    ->findOneBy([
        new SelectColumns(['id', 'post_title', 'name AS category']),
        new TermRelationshipCondition([
            'taxonomy' => 'product_cat'
        ]),
    ]);

// $product->category will have the corresponding category name

扩展生成的查询

对于更高级的需求,也可以检索查询构建器并修改它以满足你的需求。

注意:使用select_from_eav()函数查询EAV属性。

// Fetch all products but override SELECT clause with only tree columns
$repository = $manager->getRepository(Product::class);
$result = $repository->createFindByQueryBuilder([], ['sku' => 'ASC'])
    ->select('id', 'post_title', select_from_eav('sku'))
    ->executeQuery()
    ->fetchAllAssociative();
$products = $repository->denormalize($result, Product::class . '[]');

创建一个新的术语

如果已经存在,术语不会重复。

// Create a new product category
$repository = $manager->getRepository(Term::class);
$term = $repository->createTermForTaxonomy('Jewelry', 'product_cat');

将术语添加到实体中

// Add all existing product tags to a product
$repository = $manager->getRepository(Term::class);
$repository->addTermsToEntity($product, $repository->findByTaxonomy('product_tag'));

从实体中移除术语

// Remove all existing product tags from a product
$repository = $manager->getRepository(Term::class);
$repository->removeTermsFromEntity($product, $repository->findByTaxonomy('product_tag'));

字段更新

在更新之前有一个类型验证。你不能将字符串分配给日期字段、整数字段等。

$repository = $manager->getRepository(Post::class);
$repository->updatePostTitle(4, 'New title');
$repository->updatePostContent(4, 'New content');
$repository->updatePostDate(4, new \DateTime());
// Alternative
$repository->updateSingleField(4, 'post_status', 'publish');

实体创建或更新

一次性创建或更新实体及其所有字段。

限制

  • 只有基本字段(在wp_posts表中的列)被持久化,不是EAV。
  • 在对象创建或更新之前必须填写所有属性,因为架构不支持NULL值。
  • 没有更改跟踪
$repository = $manager->getRepository(Post::class);
$post = $repository->findOneByPostTitle('My post');
$post->postTitle = 'A new title for my post';
$post->postStatus = 'publish';
$repository->persist($post);
// or directly calling the EntityManager
$manager->persist($post);

实体重复

使用DuplicationService复制一个具有所有EAV属性和术语的实体。生成的实体已经持久化并具有新的ID。

$duplicationService = $registry->get(DuplicationService::class);
// or
$duplicationService = DuplicationService::create($manager);

// Duplicate by ID
$newProduct =  $duplicationService->duplicate(23, Product::class);

// Duplicate by object
$product = $manager->getRepository(Product::class)->findOneBySku('woo-hoodie-with-zipper');
$newProduct =  $duplicationService->duplicate($product);

可用的实体和存储库

  • PostPostRepository
  • PagePageRepository
  • AttachmentAttachmentRepository
  • OptionOptionRepository
  • PostMetaPostMetaRepository
  • CommentCommentRepository
  • TermTermRepository
  • TermTaxonomyTermTaxonomyRepository
  • UserUserRepository
  • ProductProductRepository(WooCommerce)
  • ShopOrderShopOrderRepository(WooCommerce)
  • ShopOrderItemShopOrderItemRepository(WooCommerce)

获取选项值

为了检索WordPress选项,你有几种选择

// Query the option name yourself
$blogName = $manager->getRepository(Option::class)->find('blogname');

// Use a predefined getter
$blogName = $manager->getRepository(Option::class)->findBlogName();

// If there isn't a predefined getter, use a magic method.
// Here we get the 'active_plugins' option, automatically unserialized.
$plugins = $manager->getRepository(Option::class)->findActivePlugins();

创建自己的实体和存储库

假设你有一个自定义帖子类型名为project

首先创建一个简单的实体

// App/Wordpress/Entity/Project.php
namespace App\Wordpress\Entity;

use App\Wordpress\Repository\ProjectRepository;
use Williarin\WordpressInterop\Attributes\RepositoryClass;
use Williarin\WordpressInterop\Bridge\Entity\BaseEntity;

#[RepositoryClass(ProjectRepository::class)]
final class Project extends BaseEntity
{
}

然后创建一个存储库

// App/Wordpress/Repository/ProjectRepository.php
namespace App\Wordpress\Repository;

use App\Wordpress\Entity\Project;
use Symfony\Component\Serializer\SerializerInterface;
use Williarin\WordpressInterop\Bridge\Repository\AbstractEntityRepository;
use Williarin\WordpressInterop\EntityManagerInterface;

/**
 * @method Project|null find($id)
 * @method Project[]    findAll()
 */
final class ProjectRepository extends AbstractEntityRepository
{
    public function __construct(/* inject additional services if you need them */)
    {
        parent::__construct(Project::class);
    }
    
    protected function getPostType(): string
    {
        return 'project';
    }
    
    // Add your own methods here
}

然后这样使用它

$allProjects = $manager->getRepository(Project::class)->findAll();

如果你的实体在单独的表中,也适用,只需一些额外的配置。以ShopOrderItemRepository为例。

你必须覆盖一些常量

final class ShopOrderItemRepository extends AbstractEntityRepository
{
    protected const TABLE_NAME = 'woocommerce_order_items';
    protected const TABLE_META_NAME = 'woocommerce_order_itemmeta';
    protected const TABLE_IDENTIFIER = 'order_item_id';
    protected const TABLE_META_IDENTIFIER = 'order_item_id';
    protected const FALLBACK_ENTITY = ShopOrderItem::class;

    public function __construct()
    {
        parent::__construct(ShopOrderItem::class);
    }
}

实体和存储库继承

你可能为现有的实体(如Post)有一些自定义属性。

  1. 创建一个新的实体,它通过新字段扩展了Post
  2. 创建一个新的存储库,它通过扩展PostRepository并覆盖getEntityClassName()方法来返回你的新MyPost实体类名
  3. 向你的PostRepository添加映射字段
  4. 向你的MyPost实体添加#[RepositoryClass(MyPostRepository::class)]

贡献

所有贡献都受欢迎。

如何贡献

  1. 在这个存储库上分叉
  2. 在你的分叉上创建一个新分支
  3. 做一些更改,然后运行make test以确保一切正常,运行make fix以修复ECS和composer.json错误
  4. 使用常规提交语法提交
  5. 在这个存储库的master分支上创建一个拉取请求

许可

MIT

版权(c)2022,William Arin