{"id":19624979,"url":"https://github.com/williarin/wordpress-interop","last_synced_at":"2025-05-08T21:17:38.580Z","repository":{"id":37014646,"uuid":"450130514","full_name":"williarin/wordpress-interop","owner":"williarin","description":"Interoperability library to work with WordPress database in third party apps","archived":false,"fork":false,"pushed_at":"2024-07-26T11:23:31.000Z","size":335,"stargazers_count":65,"open_issues_count":2,"forks_count":6,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-08T21:17:19.379Z","etag":null,"topics":["dbal","doctrine","orm","wordpress"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/williarin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-20T14:30:16.000Z","updated_at":"2025-04-12T10:19:03.000Z","dependencies_parsed_at":"2024-06-18T14:45:11.343Z","dependency_job_id":"fbd88726-78e4-4e21-b202-3452cf75c155","html_url":"https://github.com/williarin/wordpress-interop","commit_stats":{"total_commits":119,"total_committers":4,"mean_commits":29.75,"dds":"0.025210084033613467","last_synced_commit":"301e3a0bdb9fd75e4c43736b8833fd3f65cd8b22"},"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williarin%2Fwordpress-interop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williarin%2Fwordpress-interop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williarin%2Fwordpress-interop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williarin%2Fwordpress-interop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/williarin","download_url":"https://codeload.github.com/williarin/wordpress-interop/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253149622,"owners_count":21861740,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dbal","doctrine","orm","wordpress"],"created_at":"2024-11-11T11:39:40.571Z","updated_at":"2025-05-08T21:17:38.544Z","avatar_url":"https://github.com/williarin.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WordPress Interop\n\n[![Github Workflow](https://github.com/williarin/wordpress-interop/workflows/Test/badge.svg)](https://github.com/williarin/wordpress-interop/actions)\n\n## Introduction\n\nThis library aims to simplify the interaction with WordPress databases through third-party applications.\nIt relies on Doctrine DBAL and looks like Doctrine ORM.\n\nIt can perform simple tasks out of the box such as querying posts, retrieving attachment data, etc.\n\nYou can extend it by adding your own repositories and querying methods.\n\n**Warning!** Although it looks like an ORM, it's not an ORM library. It doesn't have two-way data manipulation features.\nSee this as a simple WordPress database manipulation helper library.\n\n## Installation\n\nThis library can be used as standalone:\n```bash\ncomposer require williarin/wordpress-interop\n```\n\nOr with Symfony:\n```bash\ncomposer require williarin/wordpress-interop-bundle\n```\n\nFind the documentation for the Symfony bundle on [the dedicated repository](https://github.com/williarin/wordpress-interop-bundle) page.\n\n## Usage\n\n### Overview\n\n```php\n$post = $manager-\u003egetRepository(Post::class)-\u003efind(15);\n```\n\n### In detail\n\nThe first thing to do is to create an entity manager linked to your DBAL connection targeting your WordPress database.\n\n```php\n$connection = DriverManager::getConnection(['url' =\u003e 'mysql://user:pass@localhost:3306/wp_mywebsite?serverVersion=8.0']);\n\n$objectNormalizer = new ObjectNormalizer(\n    new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())),\n    new CamelCaseToSnakeCaseNameConverter(),\n    null,\n    new ReflectionExtractor()\n);\n\n$serializer = new Serializer([\n    new DateTimeNormalizer(),\n    new ArrayDenormalizer(),\n    new SerializedArrayDenormalizer($objectNormalizer),\n    $objectNormalizer,\n]);\n\n$manager = new EntityManager($connection, $serializer);\n```\n\nThen you can query the database:\n```php\n/** @var PostRepository $postRepository */\n$postRepository = $manager-\u003egetRepository(Post::class);\n$myPost = $postRepository-\u003efind(15);\n$allPosts = $postRepository-\u003efindAll();\n```\n\n## Documentation\n\n### Basic querying\n\nThis works with any entity inherited from `BaseEntity`.\nBuilt-in entities are `Post`, `Page`, `Attachment` and `Product` but you can [create your own](#create-your-own-entities-and-repositories).\n\n```php\n// Fetch a post by ID\n$post = $manager-\u003egetRepository(Post::class)-\u003efind(1);\n\n// Fetch the latest published post\n$post = $manager-\u003egetRepository(Post::class)\n    -\u003efindOneByPostStatus('publish', ['post_date' =\u003e 'DESC']);\n\n// Fetch the latest published post which has 1 comment\n$post = $manager-\u003egetRepository(Post::class)\n    -\u003efindOneBy(\n        ['post_status' =\u003e 'publish', 'comment_count' =\u003e 1],\n        ['post_date' =\u003e 'DESC'],\n    );\n\n// Fetch the latest published post which has the most comments\n$post = $manager-\u003egetRepository(Post::class)\n    -\u003efindOneByPostStatus(\n        'publish',\n        ['comment_count' =\u003e 'DESC', 'post_date' =\u003e 'DESC'],\n    );\n\n// Fetch all posts which have draft or private status\n$posts = $manager-\u003egetRepository(Post::class)\n    -\u003efindByPostStatus(new Operand(['draft', 'private'], Operand::OPERATOR_IN));\n\n// Fetch all posts\n$posts = $manager-\u003egetRepository(Post::class)-\u003efindAll();\n\n// Fetch all private posts\n$posts = $manager-\u003egetRepository(Post::class)-\u003efindByPostStatus('private');\n\n// Fetch all products whose titles match regexp\n$products = $manager-\u003egetRepository(Product::class)\n    -\u003efindByPostTitle(new Operand('Hoodie.*Pocket|Zipper', Operand::OPERATOR_REGEXP));\n```\n\n### EAV querying\n\n_The term EAV refers to the [entity-attribute-value model](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model) used by WordPress through the term \"meta\" as in `wp_postmeta`, `wp_termmeta`, `wp_usermeta` etc. Here we're talking about `wp_postmeta`._\n\nThe query system supports directly querying EAV attributes.\n\nIn the example below, `sku` and `stock_status` are attributes from `wp_postmeta` table.\n\n_Note: Field names are mapped to match their property name. As an example, `_sku` becomes `sku`, or `_wc_average_rating` becomes `average_rating`._\n\n```php\n// Fetch a product by its SKU\n$product = $manager-\u003egetRepository(Product::class)-\u003efindOneBySku('woo-vneck-tee');\n\n// Fetch the latest published product which is in stock\n$product = $manager-\u003egetRepository(Product::class)\n    -\u003efindOneBy(\n        ['stock_status' =\u003e 'instock', 'post_status' =\u003e 'publish'],\n        ['post_date' =\u003e 'DESC'],\n    );\n    \n// Fetch all published products which are in stock\n$products = $manager-\u003egetRepository(Product::class)\n    -\u003efindBy(\n        ['stock_status' =\u003e 'instock', 'post_status' =\u003e 'publish'],\n        ['post_date' =\u003e 'DESC'],\n    );\n\n// Fetch all products whose sku match regexp\n$products = $manager-\u003egetRepository(Product::class)\n    -\u003efindBySku(new Operand('hoodie.*logo|zipper', Operand::OPERATOR_REGEXP));\n\n```\n\nIf you query an EAV attribute that doesn't exist in the entity, an `InvalidFieldNameException` exception will be thrown.\n\nTo allow extra dynamic properties to be queried, set `allow_extra_properties` option to `true` before the query. Careful though, options are set for the repository and not the query, which means they will apply to all further queries. \n\n```php\n$page = $manager-\u003egetRepository(Page::class)\n    -\u003esetOptions([\n        'allow_extra_properties' =\u003e true,\n    ])\n    -\u003efindOneBy([\n        new SelectColumns(['id', select_from_eav('wp_page_template')]),\n        'post_status' =\u003e 'publish',\n        'wp_page_template' =\u003e 'default',\n    ])\n;\n// $page-\u003ewpPageTemplate === 'default'\n```\n\n### Nested conditions\n\nFor more complex querying needs, you can add nested conditions.\n\n_Note: it only works with columns and not EAV attributes._\n\n```php\n// Fetch Hoodies as well as products with at least 30 comments, all of which are in stock\n$products = $manager-\u003egetRepository(Product::class)\n    -\u003efindBy([\n        new NestedCondition(NestedCondition::OPERATOR_OR, [\n            'post_title' =\u003e new Operand('Hoodie%', Operand::OPERATOR_LIKE),\n            'comment_count' =\u003e new Operand(30, Operand::OPERATOR_GREATER_THAN_OR_EQUAL),\n        ]),\n        'stock_status' =\u003e 'instock',\n    ]);\n\n// Fetch two products by their SKU and two by their ID\n$products = $manager-\u003egetRepository(Product::class)\n    -\u003efindBy([\n        new NestedCondition(NestedCondition::OPERATOR_OR, [\n            'sku' =\u003e new Operand(['woo-tshirt', 'woo-single'], Operand::OPERATOR_IN),\n            'id' =\u003e new Operand([19, 20], Operand::OPERATOR_IN),\n        ]),\n    ]);\n// count($products) === 4\n```\n\n### EAV relationship conditions\n\nQuery entities based on their EAV relationships.\n\n_Note: the EAV fields must have their original names, unlike mapped fields for direct EAV querying._\n\n```php\n// Fetch the featured image of the post with ID \"4\"\n$attachment = $manager-\u003egetRepository(Attachment::class)\n    -\u003efindOneBy([\n        new RelationshipCondition(4, '_thumbnail_id'),\n    ]);\n\n// Get featured images of posts 4, 13, 18 and 23 at once\n$attachments = $manager-\u003egetRepository(Attachment::class)\n    -\u003efindBy([\n        new RelationshipCondition(\n            new Operand([4, 13, 18, 23], Operand::OPERATOR_IN),\n            '_thumbnail_id',\n        ),\n    ]);\n\n// Same as above example but include the original ID in the result\n$attachments = $manager-\u003egetRepository(Attachment::class)\n    -\u003efindBy([\n        new RelationshipCondition(\n            new Operand([4, 13, 18, 23], Operand::OPERATOR_IN),\n            '_thumbnail_id',\n            'original_post_id',\n        ),\n    ]);\n// $attachments[0]-\u003eoriginalPostId === 4\n```\n\n\n### Term and taxonomy relationship conditions\n\nQuery entities based on their terms and taxonomies relationships.\n\n```php\n// Fetch products in the category \"Hoodies\"\n$products = $manager-\u003egetRepository(Product::class)\n    -\u003efindBy([\n        new TermRelationshipCondition([\n            'taxonomy' =\u003e 'product_cat',\n            'name' =\u003e 'Hoodies',\n        ]),\n    ]);\n```\n\nAdditionally, you can query terms from a joint entity, and specify the name of the term table.\n\nIn this example, we assume that the products have a `related_product` postmeta.\n```php\n// Fetch a product's category and the category of its related product\n$product = $manager-\u003egetRepository(Product::class)\n    -\u003efindOneBy([\n        new SelectColumns([\n            'id',\n            'main.name AS category',\n            'related.name AS related_category',\n            select_from_eav(\n                fieldName: 'related_product',\n                metaKey: 'related_product', // needed as it's not starting with an underscore\n            ),\n        ]),\n        new TermRelationshipCondition(\n            ['taxonomy' =\u003e 'product_cat'],\n            termTableAlias: 'main',\n        ),\n        new TermRelationshipCondition(\n            ['taxonomy' =\u003e 'product_cat'],\n            joinConditionField: 'related_product',\n            termTableAlias: 'related',\n        ),\n        'id' =\u003e 22,\n    ]);\n// $product-\u003ecategory === 'Hoodies'\n// $product-\u003erelatedCategory === 'Accessories'\n```\n\nIf not specified, the term table alias defaults to `t_0`, `t_1`, etc.\n\nA special operator `Operand::OPERATOR_IN_ALL` is also provided to match exactly all values in an array.\n\n```php\n// Fetch products that have both 'featured' and 'accessories' terms\n$products = $manager-\u003egetRepository(Product::class)\n    -\u003efindBy([\n        new TermRelationshipCondition([\n            'slug' =\u003e new Operand(['featured', 'accessories'], Operand::OPERATOR_IN_ALL),\n        ]),\n    ]);\n```\n\nThis operator is not limited to terms querying, but it's the most obvious use case.\n\n### Post relationship conditions\n\nQuery terms based on their posts relationships.\n\n```php\n// Fetch all terms of the product with SKU \"super-forces-hoodie\"\n// belonging to all taxonomies except \"product_tag\", \"product_type\", \"product_visibility\".\n$terms = $manager-\u003egetRepository(Term::class)\n    -\u003efindBy([\n        new SelectColumns(['taxonomy', 'name']),\n        new PostRelationshipCondition(Product::class, [\n            'post_status' =\u003e new Operand(['publish', 'private'], Operand::OPERATOR_IN),\n            'sku' =\u003e 'super-forces-hoodie',\n        ]),\n        'taxonomy' =\u003e new Operand(\n            ['product_tag', 'product_type', 'product_visibility'],\n            Operand::OPERATOR_NOT_IN,\n        ),\n    ]);\n```\n\n### Restrict selected columns\n\nQuerying all columns at once is slow, especially if you have a lot of entities to retrieve.\nYou can restrict the queried columns as the example below.\n\nIt works with base columns as well as EAV attributes.\n\n```php\n// Fetch only products title and SKU\n$products = $manager-\u003egetRepository(Product::class)\n    -\u003efindBy([\n        new SelectColumns(['post_title', 'sku']),\n        'sku' =\u003e new Operand('hoodie.*logo|zipper', Operand::OPERATOR_REGEXP),\n    ]);\n\n// Product entities are filled with null values except $postTitle and $sku\n```\n\nYou can as well select a column which doesn't have a mapped property in your entity.\n\n```php\n$product = $manager-\u003egetRepository(Product::class)\n    -\u003efindOneBy([\n        new SelectColumns(['id', 'post_title', 'name AS category']),\n        new TermRelationshipCondition([\n            'taxonomy' =\u003e 'product_cat'\n        ]),\n    ]);\n\n// $product-\u003ecategory will have the corresponding category name\n```\n\n### Extending the generated query\n\nFor advanced needs, it's also possible to retrieve the query builder and modify it to your needs.\n\n_Note: use `select_from_eav()` function to query EAV attributes._\n```php\n// Fetch all products but override SELECT clause with only tree columns\n$repository = $manager-\u003egetRepository(Product::class);\n$result = $repository-\u003ecreateFindByQueryBuilder([], ['sku' =\u003e 'ASC'])\n    -\u003eselect('id', 'post_title', select_from_eav('sku'))\n    -\u003eexecuteQuery()\n    -\u003efetchAllAssociative();\n$products = $repository-\u003edenormalize($result, Product::class . '[]');\n```\n\n### Create a new term\n\nTerms are not duplicated if already existing.\n\n```php\n// Create a new product category\n$repository = $manager-\u003egetRepository(Term::class);\n$term = $repository-\u003ecreateTermForTaxonomy('Jewelry', 'product_cat');\n```\n\n### Add terms to an entity\n\n```php\n// Add all existing product tags to a product\n$repository = $manager-\u003egetRepository(Term::class);\n$repository-\u003eaddTermsToEntity($product, $repository-\u003efindByTaxonomy('product_tag'));\n```\n\n### Remove terms from an entity\n\n```php\n// Remove all existing product tags from a product\n$repository = $manager-\u003egetRepository(Term::class);\n$repository-\u003eremoveTermsFromEntity($product, $repository-\u003efindByTaxonomy('product_tag'));\n```\n\n### Field update\nThere's a type validation before update.\nYou can't assign a string to a date field, a string to an int field, etc.\n\n```php\n$repository = $manager-\u003egetRepository(Post::class);\n$repository-\u003eupdatePostTitle(4, 'New title');\n$repository-\u003eupdatePostContent(4, 'New content');\n$repository-\u003eupdatePostDate(4, new \\DateTime());\n// Alternative\n$repository-\u003eupdateSingleField(4, 'post_status', 'publish');\n```\n\n### Entity creation or update\nCreate or update an entity with all its fields at once.\n\nLimitations:\n* Only the base fields (the columns in `wp_posts` table) are persisted, not the EAV\n* All properties must be filled before object creation or update as the schema doesn't support NULL values\n* No change tracking\n\n```php\n$repository = $manager-\u003egetRepository(Post::class);\n$post = $repository-\u003efindOneByPostTitle('My post');\n$post-\u003epostTitle = 'A new title for my post';\n$post-\u003epostStatus = 'publish';\n$repository-\u003epersist($post);\n// or directly calling the EntityManager\n$manager-\u003epersist($post);\n```\n\n### Entity duplication\nDuplicate an entity with all its EAV attributes and terms with `DuplicationService`.\nThe resulting entity is already persisted and has a new ID.\n\n```php\n$duplicationService = $registry-\u003eget(DuplicationService::class);\n// or\n$duplicationService = DuplicationService::create($manager);\n\n// Duplicate by ID\n$newProduct =  $duplicationService-\u003eduplicate(23, Product::class);\n\n// Duplicate by object\n$product = $manager-\u003egetRepository(Product::class)-\u003efindOneBySku('woo-hoodie-with-zipper');\n$newProduct =  $duplicationService-\u003eduplicate($product);\n```\n\n### Available entities and repositories\n\n* `Post` and `PostRepository`\n* `Page` and `PageRepository`\n* `Attachment` and `AttachmentRepository`\n* `Option` and `OptionRepository`\n* `PostMeta` and `PostMetaRepository`\n* `Comment` and `CommentRepository`\n* `Term` and `TermRepository`\n* `TermTaxonomy` and `TermTaxonomyRepository`\n* `User` and `UserRepository`\n* `Product` and `ProductRepository` (WooCommerce)\n* `ShopOrder` and `ShopOrderRepository` (WooCommerce)\n* `ShopOrderItem` and `ShopOrderItemRepository` (WooCommerce)\n\n### Get an option value\n\nTo retrieve a WordPress option, you have several choices:\n```php\n// Query the option name yourself\n$blogName = $manager-\u003egetRepository(Option::class)-\u003efind('blogname');\n\n// Use a predefined getter\n$blogName = $manager-\u003egetRepository(Option::class)-\u003efindBlogName();\n\n// If there isn't a predefined getter, use a magic method.\n// Here we get the 'active_plugins' option, automatically unserialized.\n$plugins = $manager-\u003egetRepository(Option::class)-\u003efindActivePlugins();\n```\n\n### Create your own entities and repositories\n\nSay you have a custom post type named `project`.\n\nFirst you create a simple entity:\n\n```php\n// App/Wordpress/Entity/Project.php\nnamespace App\\Wordpress\\Entity;\n\nuse App\\Wordpress\\Repository\\ProjectRepository;\nuse Williarin\\WordpressInterop\\Attributes\\RepositoryClass;\nuse Williarin\\WordpressInterop\\Bridge\\Entity\\BaseEntity;\n\n#[RepositoryClass(ProjectRepository::class)]\nfinal class Project extends BaseEntity\n{\n}\n```\n\nThen a repository:\n\n```php\n// App/Wordpress/Repository/ProjectRepository.php\nnamespace App\\Wordpress\\Repository;\n\nuse App\\Wordpress\\Entity\\Project;\nuse Symfony\\Component\\Serializer\\SerializerInterface;\nuse Williarin\\WordpressInterop\\Bridge\\Repository\\AbstractEntityRepository;\nuse Williarin\\WordpressInterop\\EntityManagerInterface;\n\n/**\n * @method Project|null find($id)\n * @method Project[]    findAll()\n */\nfinal class ProjectRepository extends AbstractEntityRepository\n{\n    public function __construct(/* inject additional services if you need them */)\n    {\n        parent::__construct(Project::class);\n    }\n    \n    protected function getPostType(): string\n    {\n        return 'project';\n    }\n    \n    // Add your own methods here\n}\n```\nThen use it like this:\n```php\n$allProjects = $manager-\u003egetRepository(Project::class)-\u003efindAll();\n```\n\nIt also works if your entity is in a separate table, with some additional configuration.\nTake as an example [ShopOrderItemRepository](src/Bridge/Repository/ShopOrderItemRepository.php).\n\nYou'll have to override some constants:\n```php\nfinal class ShopOrderItemRepository extends AbstractEntityRepository\n{\n    protected const TABLE_NAME = 'woocommerce_order_items';\n    protected const TABLE_META_NAME = 'woocommerce_order_itemmeta';\n    protected const TABLE_IDENTIFIER = 'order_item_id';\n    protected const TABLE_META_IDENTIFIER = 'order_item_id';\n    protected const FALLBACK_ENTITY = ShopOrderItem::class;\n\n    public function __construct()\n    {\n        parent::__construct(ShopOrderItem::class);\n    }\n}\n```\n\n### Entity and repository inheritance\n\nYou might have some custom attributes for existing entities such as `Post`.\n\n1. Create a new entity that extends `Post` with new fields\n2. Create a new repository that extends `PostRepository` and override `getEntityClassName()` method to return your new `MyPost` entity class name\n3. Add mapped fields to your `PostRepository`\n4. Add `#[RepositoryClass(MyPostRepository::class)]` to your `MyPost` entity\n\n## Contributing\n\nAll contributions are welcome.\n\nHow to contribute:\n1. Fork this repository\n2. Create a new branch on your fork\n3. Make some changes then run `make test` to ensure everything works, and `make fix` to fix ECS and `composer.json` errors\n4. Commit using [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) syntax\n5. Create a pull request on `master` branch of this repository\n\n## License\n\n[MIT](LICENSE)\n\nCopyright (c) 2022, William Arin\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwilliarin%2Fwordpress-interop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwilliarin%2Fwordpress-interop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwilliarin%2Fwordpress-interop/lists"}