GithubHelp home page GithubHelp logo

corcel / corcel Goto Github PK

View Code? Open in Web Editor NEW
4.2K 4.2K 590.0 1.57 MB

Use WordPress backend with Laravel or any PHP application

License: MIT License

PHP 100.00%
corcel eloquent hacktoberfest laravel mvc php shortcode taxonomies wordpress

corcel's Introduction

Corcel PHP

A collection of Model classes that allows you to get data directly from a WordPress database.

Actions Status Packagist Packagist Test Coverage Maintainability

Corcel is a collection of PHP classes built on top of Eloquent ORM (from Laravel framework), that provides a fluent interface to connect and get data directly from a WordPress database.

You can use WordPress as the backend (administration panel) or CMS, for inserting posts, custom types, etc, and any other PHP app in the other side querying those data (as a Model layer). It's easier to use Corcel with Laravel, but you're free to use it with any PHP project that uses Composer.

Buy me a Coffee | Follow Corcel on Twitter

Table of Contents

Installing Corcel

Version Compatibility

Laravel Corcel
5.1.x ~2.1.0
5.2.x ~2.2.0
5.3.x ~2.3.0
5.4.x ~2.4.0
5.5.x ~2.5.0
5.6.x ~2.6.0
5.7.x ~2.7.0
5.8.x ~2.8.0
6.0.x ^3.0.0
7.0.x ^4.0.0
8.0.x ^5.0.0
9.0.x ^6.0.0
10.0.x ^7.0.0
11.0.x ^8.0.0

Installing Corcel

You need to use Composer to install Corcel into your project:

composer require jgrossi/corcel

Configuring (Laravel)

Laravel 5.5 and newer

Corcel wil register itself using Laravel's Auto Discovery.

Laravel 5.4 and older

You'll have to include CorcelServiceProvider in your config/app.php:

'providers' => [
    /*
     * Package Service Providers...
     */
    Corcel\Laravel\CorcelServiceProvider::class,
]

Publishing the configuration file

Now configure our config file to make sure your database is set correctly and to allow you to register custom post types and shortcodes in a very easy way:

Run the following Artisan command in your terminal:

php artisan vendor:publish --provider="Corcel\Laravel\CorcelServiceProvider"

Now you have a config/corcel.php config file, where you can set the database connection with WordPress tables and much more.

Database Setup

Laravel Setup

Just set the database connection you want to be used by Corcel in config/corcel.php.

Let' suppose you have those following database connections in your config/database.php file:

// File: /config/database.php

'connections' => [

    'mysql' => [ // for Laravel database
        'driver'    => 'mysql',
        'host'      => 'localhost',
        'database'  => 'mydatabase',
        'username'  => 'admin'
        'password'  => 'secret',
        'charset'   => 'utf8',
        'collation' => 'utf8_unicode_ci',
        'prefix'    => '',
        'strict'    => false,
        'engine'    => null,
    ],

    'wordpress' => [ // for WordPress database (used by Corcel)
        'driver'    => 'mysql',
        'host'      => 'localhost',
        'database'  => 'mydatabase',
        'username'  => 'admin',
        'password'  => 'secret',
        'charset'   => 'utf8',
        'collation' => 'utf8_unicode_ci',
        'prefix'    => 'wp_',
        'strict'    => false,
        'engine'    => null,
    ],
],

In this case you should want to use the wordpress connection for Corcel, so just set it into the Corcel config file config/corcel.php:

'connection' => 'wordpress',

Other PHP Framework (not Laravel) Setup

Here you have to configure the database to fit the Corcel requirements. First, you should include the Composer autoload file if not already loaded:

require __DIR__ . '/vendor/autoload.php';

Now you must set your WordPress database params:

$params = [
    'database'  => 'database_name',
    'username'  => 'username',
    'password'  => 'pa$$word',
    'prefix'    => 'wp_' // default prefix is 'wp_', you can change to your own prefix
];
Corcel\Database::connect($params);

You can specify all Eloquent params, but some are default (but you can override them).

'driver'    => 'mysql',
'host'      => 'localhost',
'charset'   => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix'    => 'wp_', // Specify the prefix for WordPress tables, default prefix is 'wp_'

Usage

Posts

Every time you see Post::method(), if you're using your own Post class (where you set the connection name), like App\Post you should use App\Post::method() and not Post::method(). All the examples are assuming you already know this difference.

In the examples, every time you see Post::method() assume Corcel\Model\Post::method().

// All published posts
$posts = Post::published()->get();
$posts = Post::status('publish')->get();

// A specific post
$post = Post::find(31);
echo $post->post_title;

Creating your own model classes

Optionally you can create your own Post model (or Page, or whatever) which extends Corcel\Post. Then set the connection name (if you want to override the Corcel's default one) you're using, in this case foo-bar:

Extending Corcel\Model\Post class can add flexibility to your project, once you can add custom methods and logic, according what you need to use from your WordPress database.

<?php // File: app/Post.php

namespace App;

use Corcel\Model\Post as Corcel;

class Post extends Corcel
{
    protected $connection = 'foo-bar';

    public function customMethod() {
        //
    }
}

So, now you can fetch WP database data using your own class:

$posts = App\Post::all(); // using the 'foo-bar' connection

Just remember you don't have to extends our Post class, you can use Corcel\Model\Post and all others model without any problem.

Meta Data (Custom Fields)

NOTE: In Corcel v1 you could save meta data using the Post::save() method. That's not allowed anymore. Use saveMeta() or createMeta() (see below) methods to save post meta.

You can retrieve meta data from posts too.

// Get a custom meta value (like 'link' or whatever) from a post (any type)
$post = Post::find(31);
echo $post->meta->link; // OR
echo $post->fields->link;
echo $post->link; // OR

To create or update meta data form a User just use the saveMeta() or saveField() methods. They return bool like the Eloquent save() method.

$post = Post::find(1);
$post->saveMeta('username', 'jgrossi');

You can save many meta data at the same time too:

$post = Post::find(1);
$post->saveMeta([
    'username' => 'jgrossi',
    'url' => 'http://jgrossi.com',
]);

You also have the createMeta() and createField() methods, that work like the saveX() methods, but they are used only for creation and return the PostMeta created instance, instead of bool.

$post = Post::find(1);
$postMeta = $post->createMeta('foo', 'bar'); // instance of PostMeta class
$trueOrFalse = $post->saveMeta('foo', 'baz'); // boolean

Querying Posts by Custom Fields (Meta)

There are multiples possibilities to query posts by their custom fields (meta) by using scopes on a Post (or another other model which uses the HasMetaFields trait) class:

To check if a meta key exists, use the hasMeta() scope:

// Finds a published post with a meta flag.
$post = Post::published()->hasMeta('featured_article')->first();

If you want to precisely match a meta-field, you can use the hasMeta() scope with a value.

// Find a published post which matches both meta_key and meta_value.
$post = Post::published()->hasMeta('username', 'jgrossi')->first();

If you need to match multiple meta-fields, you can also use the hasMeta() scope passing an array as parameter:

$post = Post::hasMeta(['username' => 'jgrossi'])->first();
$post = Post::hasMeta(['username' => 'jgrossi', 'url' => 'jgrossi.com'])->first();
// Or just passing the keys
$post = Post::hasMeta(['username', 'url'])->first();

If you need to match a case-insensitive string, or match with wildcards, you can use the hasMetaLike() scope with a value. This uses an SQL LIKE operator, so use '%' as a wildcard operator.

// Will match: 'J Grossi', 'J GROSSI', and 'j grossi'.
$post = Post::published()->hasMetaLike('author', 'J GROSSI')->first();

// Using % as a wildcard will match: 'J Grossi', 'J GROSSI', 'j grossi', 'Junior Grossi' etc.
$post = Post::published()->hasMetaLike('author', 'J%GROSSI')->first();

Fields Aliases

The Post class has support to "aliases", so if you check the Post class you should note some aliases defined in the static $aliases array, like title for post_title and content for post_content.

$post = Post::find(1);
$post->title === $post->post_title; // true

If you're extending the Post class to create your own class you can use $aliases too. Just add new aliases to that static property inside your own class and it will automatically inherit all aliases from parent Post class:

class A extends \Corcel\Post
{
    protected static $aliases = [
        'foo' => 'post_foo',
    ];
}

$a = A::find(1);
echo $a->foo;
echo $a->title; // from Post class

Custom Scopes

To order posts you can use newest() and oldest() scopes, for both Post and User classes:

$newest = Post::newest()->first();
$oldest = Post::oldest()->first();

Pagination

To order posts just use Eloquent paginate() method:

$posts = Post::published()->paginate(5);
foreach ($posts as $post) {
    // ...
}

To display the pagination links just call the links() method:

{{ $posts->links() }}

Advanced Custom Fields (ACF)

If you want to retrieve a custom field created by the Advanced Custom Fields (ACF) plugin, you have to install the corcel/acf plugin - click here for more information - and call the custom field like this:

$post = Post::find(123);
echo $post->acf->some_radio_field;
$repeaterFields = $post->acf->my_repeater_name;

To avoid unnecessary SQL queries just set the field type you're requesting. Usually two SQL queries are necessary to get the field type, so if you want to specify it you're skipping those extra queries:

$post = Post::find(123);
echo $post->acf->text('text_field_name');
echo $post->acf->boolean('boolean_field_name');

Custom Post Type

You can work with custom post types too. You can use the type(string) method or create your own class.

// using type() method
$videos = Post::type('video')->status('publish')->get();

// using your own class
class Video extends Corcel\Post
{
    protected $postType = 'video';
}
$videos = Video::status('publish')->get();

Using type() method will make Corcel to return all objects as Corcel\Post. Using your custom class you have the advantage to customize classes, including custom methods and properties, return all objects as Video, for example.

Custom post types and meta data:

// Get 3 posts with custom post type (store) and show its address
$stores = Post::type('store')->status('publish')->take(3)->get();
foreach ($stores as $store) {
    $storeAddress = $store->address; // option 1
    $storeAddress = $store->meta->address; // option 2
    $storeAddress = $store->fields->address; // option 3
}

Configuring the returning Instance

Every time you call something like Post::type('video)->first() or Video::first() you receive a Corcel\Model\Post instance.

If you choose to create a new class for your custom post type, you can have this class be returned for all instances of that post type.

Registering Post Types (the easy way)

Instead of call Post::registerPostType() method for all custom post type you want to register, just use the Corcel's config file and map all custom posts and it's classes. They will be registered automatically for you:

'post_types' => [
    'video' => App\Video::class,
    'foo' => App\Foo::class,
]

So every time you query a custom post type the mapped instance will be returned.

This is particular useful when you are intending to get a Collection of Posts of different types (e.g. when fetching the posts defined in a menu).

Registering Post Types (the hard way)

//all objects in the $videos Collection will be instances of Post
$videos = Post::type('video')->status('publish')->get();

// register the video custom post type and its particular class
Post::registerPostType('video', '\App\Video')


//now all objects in the $videos Collection will be instances of Video
$videos = Post::type('video')->status('publish')->get();

You can also do this for inbuilt classes, such as Page or Post. Simply register the Page or Post class with the associated post type string, and that object will be returned instead of the default one.

Shortcodes

From config (Laravel)

You can map all shortcodes you want inside the config/corcel.php file, under the 'shortcodes' key. In this case you should create your own class that implements the Corcel\Shortcode interface, that requires a render() method:

'shortcodes' => [
    'foo' => App\Shortcodes\FooShortcode::class,
    'bar' => App\Shortcodes\BarShortcode::class,
],

This is a sample shortcode class:

class FakeShortcode implements \Corcel\Shortcode
{
    /**
     * @param ShortcodeInterface $shortcode
     * @return string
     */
    public function render(ShortcodeInterface $shortcode)
    {
        return sprintf(
            'html-for-shortcode-%s-%s',
            $shortcode->getName(),
            $shortcode->getParameter('one')
        );
    }
}

In runtime

You can add shortcodes by calling the addShortcode method on the Post model :

// [gallery id="1"]
Post::addShortcode('gallery', function ($shortcode) {
    return $shortcode->getName() . '.' . $shortcode->getParameter('id');
});
$post = Post::find(1);
echo $post->content;

Laravel 5.5 uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider

If you are using Laravel, we suggest adding your shortcodes handlers in App\Providers\AppServiceProvider, in the boot method.

Shortcode Parsing

Shortcodes are parsed with the thunderer/shortcode library.

Several different parsers are provided. RegularParser is the most technically correct and is provided by default. This is suitable for most cases. However if you encounter some irregularities in your shortcode parsing, you may need to configure Corcel to use the WordpressParser, which more faithfully matches WordPress' shortcode regex. To do this, if you are using Laravel, edit the config/corcel.php file, and uncomment your preferred parser. Alternatively, you can replace this with a parser of your own.

'shortcode_parser' => Thunder\Shortcode\Parser\RegularParser::class,
// 'shortcode_parser' => Thunder\Shortcode\Parser\WordpressParser::class,

If you are not using Laravel, you can to do this in runtime, calling the setShortcodeParser() method from any class which uses the Shortcodes trait, such as Post, for example.

$post->setShortcodeParser(new WordpressParser());
echo $post->content; // content parsed with "WordpressParser" class

For more information about the shortcode package, click here.

Taxonomies

You can get taxonomies for a specific post like:

$post = Post::find(1);
$taxonomy = $post->taxonomies()->first();
echo $taxonomy->taxonomy;

Or you can search for posts using its taxonomies:

$post = Post::taxonomy('category', 'php')->first();

Post Format

You can also get the post format, like the WordPress function get_post_format():

echo $post->getFormat(); // should return something like 'video', etc

Pages

Pages are like custom post types. You can use Post::type('page') or the Corcel\Model\Page class.

use Corcel\Model\Page;

// Find a page by slug
$page = Page::slug('about')->first(); // OR
$page = Post::type('page')->slug('about')->first();
echo $page->post_title;

Categories and Taxonomies

Get a category or taxonomy or load posts from a certain category. There are multiple ways to achieve it.

// all categories
$cat = Taxonomy::category()->slug('uncategorized')->posts->first();
echo "<pre>"; print_r($cat->name); echo "</pre>";

// only all categories and posts connected with it
$cat = Taxonomy::where('taxonomy', 'category')->with('posts')->get();
$cat->each(function($category) {
    echo $category->name;
});

// clean and simple all posts from a category
$cat = Category::slug('uncategorized')->posts->first();
$cat->posts->each(function($post) {
    echo $post->post_title;
});

Attachment and Revision

Getting the attachment and/or revision from a Post or Page.

$page = Page::slug('about')->with('attachment')->first();
// get feature image from page or post
print_r($page->attachment);

$post = Post::slug('test')->with('revision')->first();
// get all revisions from a post or page
print_r($post->revision);

Thumbnails

Getting the thumbnail for a Post or Page.

$post = Post::find(1);

// Retrieve an instance of Corcel\Model\Meta\ThumbnailMeta.
print_r($post->thumbnail);

// For convenience you may also echo the thumbnail instance to get the URL of the original image.
echo $post->thumbnail;

To retrieve a particular thumbnail size you may call the ->size() method on the thumbnail object and pass in a thumbnail size string parameter (e.g. thumbnail or medium). If the thumbnail has been generated, this method returns an array of image metadata, otherwise the original image URL will be returned as a fallback.

if ($post->thumbnail !== null) {
    /**
     * [
     *     'file' => 'filename-300x300.jpg',
     *     'width' => 300,
     *     'height' => 300,
     *     'mime-type' => 'image/jpeg',
     *     'url' => 'http://localhost/wp-content/uploads/filename-300x300.jpg',
     * ]
     */
    print_r($post->thumbnail->size(Corcel\Model\Meta\ThumbnailMeta::SIZE_THUMBNAIL));

    // http://localhost/wp-content/uploads/filename.jpg
    print_r($post->thumbnail->size('invalid_size'));
}

Options

In previous versions of Corcel this classe was called Options instead of Option (singular). So take care of using always this class in the singular form starting from v2.0.0.

The Option::getAll() method was removed in Corcel 2+, in favor of Option::asArray($keys []).

You can use the Option class to get data from wp_options table:

$siteUrl = Option::get('siteurl');

You can also add new options:

Option::add('foo', 'bar'); // stored as string
Option::add('baz', ['one' => 'two']); // this will be serialized and saved

You can get all options in a simple array:

$options = Option::asArray();
echo $options['siteurl'];

Or you can specify only the keys you want to get:

$options = Option::asArray(['siteurl', 'home', 'blogname']);
echo $options['home'];

Menu

To get a menu by its slug, use the syntax below. The menu items will be loaded in the items variable (it's a collection of Corcel\Model\MenuItem objects).

The currently supported menu items are: Pages, Posts, Custom Links and Categories.

Once you'll have instances of MenuItem class, if you want to use the original instance (like the original Page or Term, for example), just call the MenuItem::instance() method. The MenuItem object is just a post with post_type equals nav_menu_item:

$menu = Menu::slug('primary')->first();

foreach ($menu->items as $item) {
    echo $item->instance()->title; // if it's a Post
    echo $item->instance()->name; // if it's a Term
    echo $item->instance()->link_text; // if it's a custom link
}

The instance() method will return the matching object:

  • Post instance for post menu item;
  • Page instance for page menu item;
  • CustomLink instance for custom menu item;
  • Term instance for category menu item.

Multi-levels Menus

To handle multi-levels menus, loop through all the menu items to put them on the right levels, for example.

You can use the MenuItem::parent() method to retrieve the parent instance of that menu item:

$items = Menu::slug('foo')->first()->items;
$parent = $items->first()->parent(); // Post, Page, CustomLink or Term (category)

To group menu items according their parents, you can use the ->groupBy() method in the $menu->items collection, grouping menu items by their $item->parent()->ID.

To read more about the groupBy() method take a look on the Laravel documentation.

Users

You can manipulate users in the same manner you work with posts:

// All users
$users = User::get();

// A specific user
$user = User::find(1);
echo $user->user_login;

Authentication

Using Laravel

If you're using Laravel 5.4 or older, make sure you have the CorcelServiceProvider provider registered.

And then, define the user provider in config/auth.php to allow Laravel to login with WordPress users:

'providers' => [
    'users' => [
        'driver' => 'corcel',
        'model'  => Corcel\Model\User::class,
    ],
],

Now you can use the Auth facade to authenticate users:

Auth::validate([
    'email' => '[email protected]', // or using 'username' too
    'password' => 'secret',
]);

To make Laravel's Password Reset work with Corcel, we have to override how passwords are stored in the database. To do this, you must change Auth/PasswordController.php from:

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;

class PasswordController extends Controller
{
    use ResetsPasswords;

to

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Corcel\Laravel\Auth\ResetsPasswords as CorcelResetsPasswords;

class PasswordController extends Controller
{
    use ResetsPasswords, CorcelResetsPasswords {
        CorcelResetsPasswords::resetPassword insteadof ResetsPasswords;
    }

Not using Laravel

You can use the AuthUserProvider class to manually authenticate a user :

$userProvider = new Corcel\Laravel\Auth\AuthUserProvider;
$user = $userProvider->retrieveByCredentials(['username' => 'admin']);
if(!is_null($user) && $userProvider->validateCredentials($user, ['password' => 'admin'])) {
    // successfully login
}

Remember you can use both username and email as credentials for a User.

Running Tests

To run the phpunit tests, execute the following command :

./vendor/bin/phpunit

If you have the global phpunit command installed you can just type:

phpunit

All tests were written using Sqlite with :memory database, so it runs in your memory. All tests use factories and migrations. Take a look on tests/database/factories and tests/database/migrations directories for more information.

Contributing

All contributions are welcome to help improve Corcel.

Before you submit your Pull Request (PR) consider the following guidelines:

  • Fork https://github.com/corcel/corcel in Github;

  • Clone your forked repository (not Corcel's) locally and create your own branch based on the version you want to fix (2.1, 2.2, 2.3, 2.4 or 2.5): git checkout -b my-fix-branch 2.5;

  • Make all code changes. Remember here to write at least one test case for any feature you add or any bugfix (if it's not tested yet). Our goal is to have 100% of the code covered by tests, so help us to write a better code ;-) If you don' have experience with tests it's a good opportunity to learn. Just take a look into our tests cases and you'll see how simple they are.

  • Run the unit tests locally to make sure your changes did not break any other piece of code;

  • Push your new branch to your forked repository, usually git push origin HEAD should work;

  • In GitHub again, create a Pull Request (PR) from your custom my-fix-branch branch (from your forked repository) to the related branch (corcel:2.5, for example, not corcel:master, please;

  • Wait for the approval :-)

Licence

MIT License ยฉ Junior Grossi

corcel's People

Contributors

adrianogl avatar atmonshi avatar aurlin avatar bluehaoran avatar carusogabriel avatar codeserk avatar crynobone avatar dartui avatar dougsisk avatar jakejohns avatar jeffersonsouza avatar jgrossi avatar jjiko avatar jpmurray avatar jubeki avatar jwdunne avatar kriskbx avatar leocaseiro avatar lucasmarques73 avatar luizpedone avatar macedd avatar matt-hullo avatar mattkendon avatar mikepsinn avatar rundef avatar theolampert avatar tlaverdure avatar ybr-nx avatar yoramdelangen avatar zubairsoft avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

corcel's Issues

Dependencies in composer.json

Hey, thanks for awesome package.
This part

 "php":  ">=5.3.0",
"illuminate/database": "*"

is a bit confusing in your composer.json, since latest illuminate/database require php >=5.6.4

Multilanguage

Hello,

It would be cool to integrate a multilanguage feature. The task would be, find a good multilanguage plugin, and get the posts in different languages. I think this would be great, as we could ask for posts in different languages. The problem is we will be stuck with the dependence of one particular plugin.

What do you think?

Should post_type be fillable on Post model

Currently the only fillable attributes for a Post are post_content, post_title, post_excerpt, to_ping, pinged, and post_content_filtered. At the moment if I want to create a new Page using Page::create($attributes) I have to manually set the post_type separately and resave the post, or have the post saved with a post_type of 'post' as default.

Should post_type therefore be fillable, or is that considered dangerous?

Any thoughts?

Yoast SEO

Hello,

I normally use Yoast SEO plugin for WP and it's quite handy. However, getting all the metas, one by one can be a pain in the ass, as they are like _yoast_wpseo_focuskw and awful things like that... I'm definitely going to translate that to something more legible. The question is if that would make sense in your project, or if I should do that in an upper layer in my projects.

My idea for your project was creating a new field called seo in Posts which contained all the meaningful data taken from Yoast (title, description, keywords, ...)

What do you think about this?

WordPress custom table prefix doesn't work

Adding a custom prefix to the connection params does not override the prefix used to lookup tables. The wp_ prefix is hardcoded in corcel.

i.e.

$params = array(
'database' => DB_NAME,
'username' => DB_USER,
'password' => DB_PASSWORD,
'host' => DB_HOST,
'prefix' => 'abc_'
);

Will generate PDO exceptions like: 'mydb.abc_wp_postmeta' doesn't exist' in ...

Should perhaps be looking for "abc_postmeta" and not "abc_wp_postmeta" for example.

AuthServiceProvider Class not found

Hi

I've added the authenication service provider in config/app.php

The following error occured

FatalErrorException in ProviderRepository.php line 146:
Class 'Corcel\Providers\Laravel\AuthServiceProvider' not found

Please advise us

Thanks
suresh

Laravel 5 Integration

I added the project to my composer require but I'm not able to use Post::find(31) Facade. Is there something I should know about Laravel 5 integration or is this a bug?

Here is my Controller

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Corcel;

class WordPressController extends Controller
{
    public function getIndex ()
    {
        // This stuff works
        // $posts = get_posts([
        //     'posts_per_page' => 20,
        //     'order' => 'ASC',
        //     'orderby' => 'post_title',
        //     ]);

       //This stuff does not
        $posts = Post::find(31);
        return $posts;
    }

Error Page
screen shot 2015-09-11 at 1 14 10 pm

starter site

please create a simple starter site for beginners

required phpunit

As you have phpunit as a normal dependency, in some cases it will not be compatible with others projects. I think you could move it to require-dev to avoid problems with others environments.
Beside that, great job!

I'm working on this w.eloquent project and I'll use our models for sure.

Call to undefined method Illuminate\Database\Query\Builder::hasMeta()

Hi,

The package were tested on localhost and it's OK.

But when deployed to my website, it gives error message:

ErrorException in Builder.php line 2251:
Call to undefined method Illuminate\Database\Query\Builder::hasMeta()

I checked config/database.php and made necessary changes.

Please help.

Tags and Categories

Any idea on how to implement Tags and Categories?

I really like your package, it's super useful for me. But few things are missing like search functionality, tags, categories.

I am not exactly sure how to implement it. There are three tables related to this wp_terms, wp_term_taxanomy and wp_term_relationships

I have an idea on how to do this, but it's just joining tables and fetching.

How would you do it? Models for Tags and Categories? or anything else?

Eloquent orderBy not working.

Am I just missing something or is orderBy not supported by corcel?

I am trying to arrange some custom post types from oldest to newest, but the normal eloquent orderBy seems to have no effect on the order of the outputted posts.

Why do we need Corcel\Database::connect($params);

Hi,

Post,Comment etc., all models use the default connection and I didn't notice any difference without including

Corcel\Database::connect($params); in index.php.

Would you please help me understand the purpose?

Menu class

Hi

This is great and open lots of interesting things!
One thing, I would love to have a simple menu class.
Menu::name('header')->get();

Validate users passwords againt saved passwords by wordpress

Hi,

I've implemented the users authentication but it seems that the check is not working with users passwords but the tests are working. So i dig into the password service provider, and i found that you use BcryptHasher to check the password if it's not an MD5, then made a little test:

  • I've create a user in "wp admin" using "admin" as username and "admin" as password.
  • Then i've checked the database to get the hashed password : $P$BrYiES.08ardK6pQme0LdlmQ0idrIe/
  • It's not an MD5, so i've used BcryptHasher to check it:

$hasher = new BcryptHasher();
dd($hasher->check('admin', '$P$BrYiES.08ardK6pQme0LdlmQ0idrIe/'));

And the result is false.

Post meta data array stored as serialized string are not unserializing on access

I've storing custom post meta as single record and serialized array:

["toggle" => false]
wp_postmeta

meta_id |  post_id  |  meta_key  |  meta_value
==================================================================
6       |  6        |  info      |  a:1:{s:6:"toggle";s:5:"false";}

Accessing single post meta in loop returns unserialized value. There is any way to make it return array without manual unserializing?

$books = Post::type('book')->status('publish')->get();

@foreach ($books as $book)
    // returns "string 'a:1:{s:6:"toggle";s:4:"true";}'"
    {{ var_dump($book->meta->info) }}
@endforeach

Did I missed something?

Undefined property: Corcel\Post::$meta

Hi!!
I am trying to install the package but I get this error:

Undefined property: Corcel\Post::$meta
********
public function __get($key)
    {
        if (!isset($this->$key)) {
          -->ERROR IS HERE?  return $this->meta->$key;    
        }
********

Can you guide me where can be the error?

I have this code:

$params = array(
            'database'  => 'syscover-wp',
            'username'  => 'carlos',
            'password'  => '33527667Sb',
        );
        \Corcel\Database::connect($this->params);
        $post = Post::published()->get();

thanks!!

Plan for releases ?

Hi !
Corcel looks pretty sweet ; is there any plan for making release(s) in the future so that we don't have to depend on dev-master ?

Thanks

Eagerloading post-thumbnails/featured-images

The attachment ID of a post-thumbnail/featured-image gets stored in post meta. The current method to get the image looks like this:

public function getImageAttribute()
{
    if (!is_null($this->meta->_thumbnail_id)) {
        $image = Attachment::find($this->meta->_thumbnail_id);

        if (!is_null($image)) {
            return $image->guid;
        }
    }
}

This triggers a DB request for every image. This is unacceptable on larger sites: I've got up to 400 queries on one of my newest projects. I figured out how we can eager load this images to minimize the query count. It's kinda dirty that's why I openend a ticket to discuss it before implementing it. ;)

We could do something like this:

public function thumbnail() {
    return $this->hasOne( 'Corcel\ThumbnailMeta', 'post_id' )->where( 'meta_key', '_thumbnail_id' );
}

public function getImageAttribute() {
    if ( !isset( $this->thumbnail ) ) {
        return false;
    }

    return $this->thumbnail->attachment;
}

And the ThumbnailMeta class could look like this:

class ThumbnailMeta extends PostMeta {
    protected $with = [ 'attachment' ];

    public function attachment() {
        return $this->belongsTo( 'Corcel\Attachment', 'meta_value' );
    }
}

In the end you can eagerload the thumbnails with Post::with('thumbnail')->first(). This triggers two additional DB requests instead of one for each post/image when using the solution that is implemented right now.

What do you think about this? Should we give it a shot? :D

Beautify model arguments

Hello guys, I've just started using this awesome package, but I found out the WP fields are quite awful :(

  "ID" => 3
    "post_author" => 1
    "post_date" => "2016-01-30 16:50:25"
    "post_date_gmt" => "-0001-11-30 00:00:00"
    "post_content" => ""
    "post_title" => "Auto Draft"
    "post_excerpt" => ""
    "post_status" => "auto-draft"
    "comment_status" => "open"
    "ping_status" => "open"
    "post_password" => ""
    "post_name" => ""
    "to_ping" => ""
    "pinged" => ""
    "post_modified" => "2016-01-30 16:50:25"
    "post_modified_gmt" => "-0001-11-30 00:00:00"
    "post_content_filtered" => ""
    "post_parent" => 0
    "menu_order" => 0
    "post_type" => "post"
    "post_mime_type" => ""
    "comment_count" => 0
    "meta" => []
  ]

The post_title field instead of title kills me a little bit.

Would you approve something generic to translate this ugly field names into something more legible? Maybe an array located on every model with a trait to make the conversions?

If you agree with me I can work on this :)

Have tests run against wptest.io content?

wptest.io has a wordpress dump file that tests all content types, formatting etc. I've been unable to get corcel to work with a wordpress with the file imported to it. The fix to #77 got past the taxonomy issue only to stumble at metadata. Would testing against this help that?

http://wptest.io/

Let's move Corcel up!!!

Hi guys!

First thank you for all contributions Corcel had last months :D

I'd like to propose the creation of a Slack channel. What do you think? We can add contributions (and followers) and get discussions about new improvements and features, what they want, etc.

What do you think about it? It'll be nice to add more administrators too, merging pull request, checking code, etc.

Cheers :-)

CC: @yoramdelangen @jeffersonsouza @ThiagoF @ashwin-sureshkumar @kriskbx @bruno-barros @rundef

Pagination override, why?

this code prevents the default laravel pagination from working, why is this done?

/**
 * Overrides the paginate() method to a custom and simple way.
 * 
 * @param int $perPage
 * @param int $currentPage
 * @return \Illuminate\Database\Eloquent\Collection
 */
public function paginate($perPage = 10, $currentPage = 1)
{
    $skip = $currentPage * $perPage - $perPage;
    return $this->skip($skip)->take($perPage)->get();
}

Master branch generate notices.

When i use the master-branch Post::published()->all() generates PHP-notices related to the $post->softDelete and the __get() function in Post.

It works just fine in the dev-branch. Might it be time for a merge?

Great work

Hi, Great work with adding eloquent to Wordpress ๐Ÿ˜„
This should be integrated in to Themosis, maybe do a pull request to them?

Featured image not found

I have this post with a featured image and it's not showing, am I doing something wrong?

$post = Post::slug($slug)->with('attachment')->first();
dd($post->attachment->first());

And I get this:

Collection {#190 โ–ผ
#items: []
}

the $post->ID is correct...

Assigning connection for Relationships has broken non-wordpress model relationships

The project that I'm using Corcel on has a mix of corcel models and eloquent models. The eloquent models use a different connection to the corcel models, because the corcel connection has a wp_ prefix.

However, with the recent changes to Relationship methods on the Model class, relationships across connections (but on the same database) are no longer possible. The hasMany method applies the connection of the corcel model to the related eloquent model so that the related model now looks for a table with a prefix of wp_.

By altering the connection in this way, we've made corcel models cut off from other parts of Laravel, which reduces the effectiveness of this library.

A simple fix (but breaking backwards compatibility) would be to have new relationship methods for the Corcel\Model class, instead of overriding the Eloquent\Model classes hasMany method (and others).

An alternative might be to update the hasMany method so that the boolean flag could be set altering the behaviour of applying the connection to the related model or not.

public function hasMany($related, $foreignKey = null, $localKey = null. $applyConnection = true)
    {
        $foreignKey = $foreignKey ?: $this->getForeignKey();

        $instance = new $related();

        if ($applyConnection) {
            $instance->setConnection($this->getConnection()->getName());
        }

        $localKey = $localKey ?: $this->getKeyName();

        return new HasMany($instance->newQuery(), $this, $foreignKey, $localKey);
    }

What do people think?

Laravel / Corcel ignoring $connection set in model

I've added a Post model to my app directory that looks like this

<?php

namespace App;

use Corcel\Post as Corcel;

class Post extends Corcel
{
    protected $connection = 'wordpress';
}

I've added a custom connection to my database.php file:

'wordpress' => [
    'driver'    => 'mysql',
    'host'      => env('DB_HOST', ''),
    'database'  => env('DB_DATABASE', ''),
    'username'  => env('DB_USERNAME', ''),
    'password'  => env('DB_PASSWORD', ''),
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => 'wp_',
    'strict'    => false,
    'engine'    => null,
],    

Yet when requesting posts like so:

$posts = App\Post::all();

I'm getting an error because it's using the wrong connection and not adding the wp_ prefix.

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'cms.terms' doesn't exist (SQL: select * from `terms` where `terms`.`term_id` in (1))

Thanks for the awesome work you're all doing on this!

Strict standard notices in ::paginate()

Hi.

The current master branch gives me the following notice:

Strict standards: Declaration of Corcel\PostBuilder::paginate() should be compatible with Illuminate\Database\Eloquent\Builder::paginate($perPage = NULL, $columns = Array, $pageName = 'page') in /wwwroot/vendor/jgrossi/corcel/src/Corcel/PostBuilder.php on line 14

Might be a good idea to redefine PostBuilder::paginate API or rename the function to something else?

multi-site

Hi,

Thanks for a great project!

If I am connecting to a multi-site wordpress, is there a way for me to change the prefix without needing to run \Corcel\Database::connect($params); again with the new prefix specified? This is for a project where I am connecting to our wordpress and posting articles to hundreds of sites.

right database connection, wrong prefix being used

Hey,

I'm having a play with Corcel (and Laravel) and am having an issue when changing the database connection in my Model like this:

Use Corcel\PostBuilder;

class WPCustomPost extends Corcel\Post {
    protected $connection = 'mysql-wp';
[...]

This throws Base table or view not found: 1146 Table 'db_name.prefix1_postmeta' doesn't exist

I've got 2 DB connections set up: mysql and mysql-wp .

'mysql-wp' => array(
            'driver'    => 'mysql',
            'host'      => 'localhost',
            'database'  => 'db_name',
            'username'  => 'root',
            'password'  => 'root',
            'charset'   => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix'    => 'wp_',
        )

They used to be different users and prefixes but I've narrowed it down to a prefix issue. The 'mysql-wp' database connection gets used (changing the user throws a no access error) but for some reason it's using "prefix1_" instead of "wp_".'

Any ideas why this might be happening?

If I change the PostMeta class to

namespace Corcel;
use Illuminate\Database\Eloquent\Model as Eloquent;
class PostMeta extends Eloquent
{ 
    protected $connection = 'mysql-wp';

it seems to work fine ?

new Model does not import Str class

Just run into a major bug with the base Model class. The belongsTo method references the Illuminate\Support\Str class but does not import it.

Testing started at 21:24 ...
PHPUnit 4.2.2 by Sebastian Bergmann.

Configuration read from C:\xampp\htdocs\corcel\phpunit.xml

PHP Fatal error:  Uncaught Error: Class 'Corcel\Str' not found in C:\xampp\htdocs\corcel\src\Model.php:39
Stack trace:
#0 C:\xampp\htdocs\corcel\src\PostMeta.php(32): Corcel\Model->belongsTo('Corcel\\Post')
#1 C:\xampp\htdocs\corcel\vendor\illuminate\database\Eloquent\Model.php(2689): Corcel\PostMeta->post()
#2 C:\xampp\htdocs\corcel\vendor\illuminate\database\Eloquent\Model.php(2662): Illuminate\Database\Eloquent\Model->getRelationshipFromMethod('post')
#3 C:\xampp\htdocs\corcel\src\Model.php(76): Illuminate\Database\Eloquent\Model->getRelationValue('post')
#4 C:\xampp\htdocs\corcel\vendor\illuminate\database\Eloquent\Model.php(2604): Corcel\Model->getRelationValue('post')
#5 C:\xampp\htdocs\corcel\vendor\illuminate\database\Eloquent\Model.php(3392): Illuminate\Database\Eloquent\Model->getAttribute('post')
#6 C:\xampp\htdocs\corcel\tests\PostMetaTest.php(29): Illuminate\Database\Eloquent\Model->__get('post')
#7 [internal function]: PostMetaTest->testPostRelation()
#8 C:\xampp\htdocs\corcel\vendor\phpunit\phpunit\src\Framework in C:\xampp\htdocs\corcel\src\Model.php on line 39

Fatal error: Uncaught Error: Class 'Corcel\Str' not found in C:\xampp\htdocs\corcel\src\Model.php:39
Stack trace:
#0 C:\xampp\htdocs\corcel\src\PostMeta.php(32): Corcel\Model->belongsTo('Corcel\\Post')
#1 C:\xampp\htdocs\corcel\vendor\illuminate\database\Eloquent\Model.php(2689): Corcel\PostMeta->post()
#2 C:\xampp\htdocs\corcel\vendor\illuminate\database\Eloquent\Model.php(2662): Illuminate\Database\Eloquent\Model->getRelationshipFromMethod('post')
#3 C:\xampp\htdocs\corcel\src\Model.php(76): Illuminate\Database\Eloquent\Model->getRelationValue('post')
#4 C:\xampp\htdocs\corcel\vendor\illuminate\database\Eloquent\Model.php(2604): Corcel\Model->getRelationValue('post')
#5 C:\xampp\htdocs\corcel\vendor\illuminate\database\Eloquent\Model.php(3392): Illuminate\Database\Eloquent\Model->getAttribute('post')
#6 C:\xampp\htdocs\corcel\tests\PostMetaTest.php(29): Illuminate\Database\Eloquent\Model->__get('post')
#7 [internal function]: PostMetaTest->testPostRelation()
#8 C:\xampp\htdocs\corcel\vendor\phpunit\phpunit\src\Framework in C:\xampp\htdocs\corcel\src\Model.php on line 39

Process finished with exit code 255

I ran into this when using PostMeta, but this affects a lot of classes. I've got a fix for this with a test for the PostMeta class, which I'll raise as a pull request.

pagination and meta order

if you make a paginated query ti never work but if you delete the function that overides the pagination of laravel model it works in L5 and you must extend to Model and not from Eloquent in L5 other thing is that if you try to make a join to order by some custom taxonomy it never be reorder because you internally ordered it by post date it fix can be solved making other protected variable that said if you want this post be ordered by post date

Post method __get extremely slow

I've been having some troubles with the __get($key) method in the Post model.

It makes a queries looking for metas every time a key is not found in the post model. This is very time consuming and avoids any possible cache (I have a cache and still have like 750 queries to meta files)

screen shot 2016-02-12 at 01 06 02

I'd suggest another approach, removing the __get method and getting all the needed information with accessors like done before.

What do you think?

User Models

Hi,

The user table is missing models, but isn't very hard to implement (it is like posts).
There's a Meta Model and Collection already.

Are you planning to get them in Corcel? Would accept PRs?

help pls

hello how am i supposed to render

[video width="380" height="286" webm="video.webm" loop="true" autoplay="true" preload="auto"][/video] ?

Database [wordpress] not configured.

My settings look like this, and still I can't get it to work...

'wordpress' => [
'driver' => 'mysql',
'host' => 'localhost',
'database' => '***',
'username' => '**
',
'password' => '**
*****',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => 'blei_',
'strict' => false,
'engine' => null,
],

Settings

Is there something planned for getting the WP settings?

This should be as easy as connecting to the settings table and parsing some rows in case of json isn't it? Maybe I can try and make pull request

Categories & Taxonomies not implemented

Hello,

You've mentioned how to use categories and taxonomies in Readme but it's not implemented:

Categories & Taxonomies
Get a category or taxonomy or load posts from a certain category. There are multiple ways to achieve it.

// all categories
$cat = Taxonomy::category()->slug('uncategorized')->posts()->first();
echo "<pre>"; print_r($cat->name); echo "</pre>";


// only all categories and posts connected with it
$cat = Taxonomy::where('taxonomy', 'category')->with('posts')->get();
$cat->each(function($category) {
    echo $category->name;
});

// clean and simple all posts from a category
$cat = Category::slug('uncategorized')->posts()->first();
$cat->posts->each(function($post) {
    echo $post->post_title;
});

Querying custom types using Post class

This is an awesome project!

However, in the README, you suggest that this should work:

Post::type('video')->get();

But it actually generates a query that looks like:

select * from `wp_posts` where `post_type` = ? and `post_type` = ? order by `post_date` desc

with the following bindings:

Array ( [0] => post [1] => video )

And that query's never going to return any results. It's happening because a conflicting where clause for post_type is automatically added in Post::newQuery(...).

Incidentally, this came up when I was just trying to make a query of all posts by post ID (disregarding type). Currently, it looks like I can only do this by making a subclass of Post that unsets $postType, but making a subclass that's more abstract than its parent seems really messy!

It would be nice if there was either a superclass of Post that doesn't assume a post_type, or if PostBuilder automatically removed the previously-added where clause when a new type was provided.

Test assertions increase each time the tests are run

They increase by 8 each time the full suite of tests is run. I think I've found the test that has an increasing number of assertions in it.

public function testPostOrderBy()
    {
        $posts = Post::orderBy('post_date', 'asc')->get();

        $lastDate = null;
        foreach ($posts as $post) {
            if (!is_null($lastDate)) {
                $this->assertGreaterThanOrEqual(0, strcmp($post->post_date, $lastDate));
            }
            $lastDate = $post->post_date;
        }

        $posts = Post::orderBy('post_date', 'desc')->get();

        $lastDate = null;
        foreach ($posts as $post) {
            if (!is_null($lastDate)) {
                $this->assertLessThanOrEqual(0, strcmp($post->post_date, $lastDate));
            }
            $lastDate = $post->post_date;
        }
    }

Here is an example where the number of rows in the database is definitely being affected

public function testCommentEnforceConnection()
    {
        $comment = new Comment;
        $comment->setConnection('no_prefix');
        $comment->comment_content = 'Test content';
        $comment->comment_author = 1;
        $comment->comment_post_ID = 2;
        $comment->save();

        $post = new Post;
        $post->post_content = 'Test';
        $post->save();

        $comment->post()->associate($post);
        $comment->save();

        $this->assertEquals('no_prefix', $comment->getConnectionName());
        $this->assertEquals('no_prefix', $comment->post->getConnectionName());
    }

How to paginate?

I tried to paginate results by using paged instead of paginate and I'm able to retrieve the first results:

Testimonial::published()->paged(5)

But when I try to print the pagination with:

$testimonials->render()

I get:

Call to undefined method Illuminate\Database\Query\Builder::render() 

How should I use it?

Unserialize doesn't throw error, returns false instead

This is a wierd one as it appears to have only just started occurring, or I've been otherwise missing this until now.

The PostMeta class has this useful method

public function getValueAttribute()
{
    try {
        return unserialize($this->meta_value);
     } catch (Exception $ex) {
         return $this->meta_value;
     }
}

However I have just started getting bad data being returned from this as unserialize no longer throws an error but instead just returns false when it cannot unserialize a value. php.net says that unserialize should return false when unable to unserialize a value.

When I tried testing this out using the test suite with the corcel library, I couldn't get the same results. As I have seen both, I suggest the following amendment to the code to handle cases like mine where unserialize does not play ball.

public function getValueAttribute()
    {
        try {
            $value = unserialize($this->meta_value);
            // if we get false, but the original value is not false then something has gone wrong. 
            // return the meta_value as is instead of unserializing
            // added this to handle cases where unserialize doesn't throw an error that is catchable
            return $value === false && $this->meta_value !== false ? $this->meta_value : $value;
        } catch (Exception $ex) {
            return $this->meta_value;
        }
    }

This now checks for a value of false being returned from unserialize and if so, just checks that against the meta_value. This handles those cases where an Error is not being thrown as something catchable.

However, I realise that this may not be the best solution and would be happy to hear what others had to say on the matter.

Is anyone else experiencing this?

Laravel 5

Hi I'm loading this straight into a Laravel 5 project with composer. It has automatically picked up my laravel DB settings which is great but it is looking for tables without the wp_ prefix. I have fixed by editing your source to protected $table = 'wp_posts'; but it would be nice to leave you're source un touched, how can I specify wp_ prefix but still use the laravel config DB?

Using multiple connections with Corcel

Hey there!

I'm using the newest Corcel together with Laravel 4.2. I installed Wordpress in a seperate database.

I configured Laravel to use this database

'mysql_wp' => array(
    'driver'    => 'mysql',
    'host'      => '127.0.0.1',
    'database'  => 'wordpress',
    'username'  => 'root',
    'password'  => 'mypassword',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => 'wp_',
)

When I try to use this configuration with Corcel\Database::connect($params) it will break my existing database connection.

I want to configure Corcel to use the mysql_wp connection. Is this possible?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.