GithubHelp home page GithubHelp logo

underpin-wp / underpin Goto Github PK

View Code? Open in Web Editor NEW
89.0 7.0 6.0 281 KB

A WordPress Framework that makes building scale-able plugins and themes easier.

Home Page: https://docs.underp.in

License: MIT License

PHP 100.00%
wordpress php wordpress-library composer-package wordpress-plugin-api wordpress-theme-api wordpress-plugin-library wordpress-framework hacktoberfest

underpin's Introduction

Underpin

The goal of Underpin is to provide a pattern that makes building PHP projects easier. It provides support for useful utilities that plugins need as they mature, such as a solid error logging utility, a batch processor for upgrade routines, and a decision tree class that makes extending and debugging multi-layered decisions way easier than traditional WordPress hooks.

Installation

Underpin can be installed in any place you can write code.

Via Composer

composer require underpin/underpin

Note This will add Underpin as a mu-plugin, but due to how WordPress handles must-use plugins, this does not actually add the plugin to your site. You must also manually require the file in a mu-plugin PHP file:

<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// Load Underpin, and its dependencies.
$autoload = plugin_dir_path( __FILE__ ) . 'vendor/autoload.php';

require_once( $autoload );

Manually

If you're developing Underpin directly, or simply don't want to use Composer, follow these steps to use:

  1. Clone this repository, preferably in the mu-plugins directory.
  2. Require Underpin.php, preferably as a mu-plugin.

I recently released the 3.0 version of this project, and unfortunately, with how I approached this build I did not document things as well as I should have. I KNOW shame on me. I'll get to it someday...

So many projects, so little time.

underpin's People

Contributors

alexstandiford 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

underpin's Issues

Several bugs in v3.0

e.g. unknown ServiceProvider class in Plugin_Builder_Provider

@alexstandiford If you are interested in more

composer require --dev szepeviktor/phpstan-wordpress
vendor/bin/phpstan analyze -c vendor/szepeviktor/phpstan-wordpress/extension.neon lib/ --level=0

Introduce find method in loader registry

The loader registry has a method, filter that makes it possible to filter the results of a registry easily. It would be nice if there was also a find method that would simply return the first registry item that matches a search result.

Update loader "add" method to return self

This would allow for method chaining, and could make registering things a little cleaner. Example:

underpin()->meta()->add( 'type_1', [ /*args*/])
underpin()->meta()->add( 'type_2', [ /*args*/])

vs

underpin()->meta()
          ->add( 'type_1', [ /*args*/])
          ->add( 'type_1', [ /*args*/])

Update documentation to specify exact location of `composer require` commands

It wasn't initially obvious that I needed to run composer require underpin/underpin in the root directory of Wordpress. I had to run this in an empty folder to see what the structure was before realizing where it needed to be ran.

And now that I'm through that hurdle, I still cannot figure out where to run the individual loaders like https://github.com/Underpin-WP/admin-page-loader.

To increase the adoption rate of underpin, it would seem that providing a clear walkthrough of each use case would help.

In this walkthrough it would be good to see what the suggested file structure should be.
As well as, seeing where you are installing each of the packages.
In the end, make your example available in a git repo so we can snag code snippets that you provided in the walkthrough.

Thanks so much. I'm looking for a rapid development framework to make Wordpress a bit more bearable. I'm hoping that this is it, but struggling to reverse engineer the assumptions made.

Update built-in Abstract classes to support being called via array

It's possible to create a factory class instead of extending an abstract class when registering things. It would be nice if most of the current abstract classes had some way to construct inline with an array instead of always needing to create a class.

This would make it so that super simple registered items could be done quickly, and if it were ever necessary to turn it into a proper extended class, it could be done with minimal concerns for backcompat.

Introduce better way to normalize, and hash items

Right now, there are a number of circumstances where Underpin generates a hash, and sometimes that has is generated from an array of arguments. These are all normalized in their own places using some basic sorting functionality when-necessary. It would be nice Underpin had some built-in functions to handle some annoying PHP things that are common, like:

  1. Recursive sorting of arrays
  2. Convert an array of data to be serialize-able (eg: a closure in an array will fail if serialized)
  3. Generating quick hashes, as well as secure hashes

I don't know where, but number 2 in-particular will ineviatably cause issues, or because it's not being done right, we're probably running into potential for hash collissions.

Introducing these methods would make it a little easier to work with arrays and hashes while also ensuring that current hashes work consistently.

The hash should also always prefix hashes with the string underpin at the beginning. This will allow distributed plugins that use Underpin's package command to automatically override this prefix with whatever they rename underpin to.

Ironically enough, I also think the recursinve array sorting and data normalization should probably be stored in the object cache so it can be accessed again quicker should this function need to run multiple times with the same array.

Reformat exporter to export to JSON

The exporter was originally created as a way to output registered items and display them directly in a text file, however, as I've worked with Underpin I've come to find that it would be a lot nicer if this was exported as a JSON file, instead. Not only would this be potentially useful in the future for debug purposes, but it may even make it possible to eventually generate entire plugins from a JSON file.

Update Dependency Processor to Cache sort order for actions

The dependency processor has a sorting algorithm that re-runs every time it is called. It would be much faster if this was stored in the WordPress object cache. Not only will this make this faster in the current call, any host that utilizes this caching mechanism will be able to speed up these actions significantly.

Separate declarations from side-effects

  1. move the main class to lib/Underpin.php
  2. move underpin() to lib/helpers.php
  3. autoload wordpress-autoloader.php and lib/helpers.php with Composer
  4. Provide an example of loading Underpin - see below
  5. remove autoloading from abstracts\Underpin
require_once __DIR_ . '/vendor/autoload.php';

add_action( 'plugins_loaded', '\\Underpin\\underpin' );

Accumulators may not be compatible with PHP 8.1

The problem:

PHP 8.1 has deprecated the option to dynamically set variables inside of a class. This can be worked around fairly simply using __set, or creating a set param method in the data object.

Update the extension's readme.md & composer pkgs

Hello,
Thanks for creating such a beautiful framework. I would like to use it for my upcoming WordPress projects but I found some issues where I was stuck.
Example: I tried to install the "Underpin Custom Post Type Loader" ext for my plugin but it's give me a fatal error when I tried to the readme.MD introductions. when I check the core "Underpin" plugin I found that the latest version is not supported the "underpin()" function. but the ext's readme.md files are not updated or do not have details for how it's working with the latest version.

I found some issues too when I tried to load packages via composer.
Example: Unerpin doesn't have a version declared on composer but "Underpin Custom Post Type Loader" ext have set version 2.0 for Underpin https://i.imgur.com/FtlpsTw.png :(

  • Niloy

Add methods to make decisions easier when using Accumulator

It would be nice to add a set method to the accumulator that, unlike update, will only change the value if it is not set.

This could simplify many logical scenarios where something is being extended using apply_filters, but the update only needs to be made if nothing else is currently set.

For example:

// GIVEN
$item = new class{
  use With_Subject;

  public function get_user_photo($id){
	return $this->apply_filters( 'user:photo', new Accumulator( [
		'default'        => null, // Default value is null
		'state'          => [ 'id' => $id ],
		'valid_callback' => fn ( $value ) => is_string( $value ),
	] ) );
  }
}

// NEW
$item->attach( 'user:photo', new Observer( 'key', [
	'update' => function ( $instance, Accumulator $accumulator ) {
                // Only sets if the state hasn't already been set.
		$accumulator->set( Integrations\User::class );
	},
] ) );

// OLD
$item->attach( 'user:photo', new Observer( 'key', [
	'update' => function ( $instance, Accumulator $accumulator ) {
		if($accumulator->get_state() === null) $accumulator->update( Integrations\User::class );
	},
] ) );

Upgrade Underpin to Support Mozart

Right now, Underpin has its own compiler package that works for Underpin, but it doesn't work for any external libraries used, such as [BerlinDB}(github.com/berlindb/core) or Background Processing. It would be better if Underpin could use the same compilation package as other WordPress-based composer packages use, Mozart.

In my brief time spent testing, I absolutely could not get it to work. Odds are pretty good this is because of the way that Underpin loads in its files, both in core and individual loaders.

I'd really like to not have to change how everything fundamentally loads if possible, but if that's necessary then I think it's a worthwhile endeavor.

Update loader registration instances to support chain-able setters

One problem with the method in-which inline instance creation works, is most IDEs have no way to know how to provide code hints for the arguments passed into the constructor. It would be nice if it were possible to register loader items using method chaining instead. This would make it possible to sanitize these items individually for children classes, and make the IDE provide the hints needed to actually set these up.

I personally find myself looking up the documentation to set up loader items all the dang time, and it would be nice if I didn't have to rely so heavily on the documentation to make sense of how to set these things up. I think a setter pattern would really help with this.

Current signature example:

underpin()->meta()->add( 'meta_name',  [
	'key'           => 'meta_key',
	'description'   => 'Meta description',
	'name'          => 'Meta Name',
	'default_value' => false,
	'type'          => 'post',
] );

Potential signature example:

underpin()->meta()->add( 'meta_name',(new Meta_Record_Type())
    ->set_key( 'meta_key' )
    ->set_description( 'Meta description' )
    ->set_name( 'Meta name' )
    ->set_default_value( false )
    ->set_type( 'post' ) );

This would provide a third way to register something. The array-based method, providing a class directly, and a setter-based method.

Some things to consider:

  1. The behavior of loaders is to make these values largely immutable. Once they're set, it shouldn't be possible to set them again. The setter-based method should work in this way, too, and there would need to be an easy way to ensure that these values, once set, can't be overridden afterward.
  2. This pattern would also make it possible to do something that's not currently possible - to set a loader value after something is registered. Would this be considered an anti-pattern? EG:
// Set most of the values on-registration
underpin()->meta()->add( 'meta_name',  [
	'key'           => 'meta_key',
	'description'   => 'Meta description',
	'name'          => 'Meta Name',
	'default_value' => false
] );

// Later on, after registration, set the type.
underpin()->meta()->get( 'meta_name' )->set_type( 'some_dynamic_type' );

Missing property declarations

 ------ -----------------------------------------------------------------------------
  Line   Factories/Observers/Loader.php
 ------ -----------------------------------------------------------------------------
  13     Access to an undefined property Underpin\Factories\Observers\Loader::$key.
  14     Access to an undefined property Underpin\Factories\Observers\Loader::$args.
  19     Access to an undefined property Underpin\Factories\Observers\Loader::$args.
  19     Access to an undefined property Underpin\Factories\Observers\Loader::$key.
 ------ -----------------------------------------------------------------------------

 ------ --------------------------------------------------------------------------
  Line   Factories/Observers/Trigger_Exception.php
 ------ --------------------------------------------------------------------------
  24     Access to an undefined property Underpin\Abstracts\Storage::$event_type.
  27     Access to an undefined property Underpin\Abstracts\Storage::$item.
 ------ --------------------------------------------------------------------------

 ------ --------------------------------------------------------------------------------------
  Line   Factories/Observers/Trigger_Notice.php
 ------ --------------------------------------------------------------------------------------
  22     Access to an undefined property Underpin\Factories\Observers\Trigger_Notice::$level.
  28     Access to an undefined property Underpin\Abstracts\Storage::$item.
  32     Access to an undefined property Underpin\Factories\Observers\Trigger_Notice::$level.
 ------ --------------------------------------------------------------------------------------

 ------ -------------------------------------------------------------------------
  Line   Loaders/Logger.php
 ------ -------------------------------------------------------------------------
  377    Access to protected property Underpin\Abstracts\Event_Type::$psr_level.
  382    Access to protected property Underpin\Abstracts\Event_Type::$psr_level.
  387    Access to protected property Underpin\Abstracts\Event_Type::$psr_level.
 ------ -------------------------------------------------------------------------

Introduce Middleware API

Some loader items would benefit from a middleware-type pattern. This could make it possible to register things using the array method without a class in circumstances where a class is currently required.

Use case example - enqueuing a script on the front-end:

underpin()->scripts()->add( 'test', [
        'handle'      => 'test',
        'src'         => 'path/to/script/src',
        'name'        => 'test',
        'description' => 'The description',
        'middlewares' => [
          'Underpin_Scripts\Factories\Enqueue_Script'
        ]
] );

The above example could remove the need to create a class for the test script above. With the enqueue script middleware adding the actions needed to enqueue the script for us.

This could also lead to composer packages that contain middleware for other common things, such as setting the REST URL and a nonce in a script automatically.

Taxonomy Class Trips Fatal Error

This is because the $name param is set to protected when it must be public. It should probably be removed from this class in general since this class extends Feature_Extension and inherits this variable anyway.

Introduce Integrations API

Something I've been thinking about a lot for a few years now is how nice it would be to integrate Underpin in a way that makes re-using code between plugins easier. A way to-do this would be by building different plugin "types", wrangling up as much of the common functionality as possible, and running them through some kind-of consistent pattern that can be re-used throughout other plugins.

This could drastically improve plugin development speeds, and make working with third party plugins significantly easier.

TL;DR

I think Underpin needs a way to load in pre-made integration classes that contain all of the information needed to work with different plugin types in a consistent fashion. The goal would be to make it so that long as a developer sticks to the methods that are included in the Underpin integration class their plugin should be able to be compatible with all of the plugins that work with the integration for free.

From this, we could build integration packages for Underpin that pre-make the integrations form common plugins. These could be installed, and used directly.

Example:

// Submit a form using an active forms plugin
// everything after forms() would come from a composer package that extends Underpin. Probably underpin/forms-integration or similar.
$form = underpin()->integrations()->forms()->find(['active' => true]);

if( !is_wp_error( $form ) ){
    $form->submit( 1, ['key' => 'value'] );
}

This would allow developers to integrate with several plugins at one time, and also prevent common pitfalls with coupling their code directly with a forms plugin directly.

This could also make it possible for theme developers to integrate with more plugins without re-writing a bunch of code in the process.

The Scenario:

You build a plugin for a customer that integrates Ninja Forms in some cool way. Later on, you need to do something identical for another customer, only this time you have to set it up to work with Gravity Forms, instead.

To accomplish this in Underpin, I would probably create a custom loader called integrations that would actually use whichever plugin is installed. Something like this:

// Create the integration class
abstract class Integration{
    use Feature_Extension;

    abstract public function get_form( $form_id );

    abstract public function submit( $form_id, $data );

    abstract public function fields( $form_id );
}

class Integration_Instance extends Integration{
    	use Instance_Setter;

	protected $custom_form_action;

	/**
	 * Block_Instance constructor.
	 *
	 * @param array $args Overrides to default args in the object
	 */
	public function __construct( array $args = [] ) {
		$this->set_values( $args );

		parent::__construct();
	}

	public function get_form( $form_id ){
		return $this->set_callable( $this->get_form_action, form_id );
	}

 	 abstract function submit( $form_id, $data ){
		return $this->set_callable( $this->submit_action, form_id, $data );
	}

	abstract function fields( $form_id ){
		return $this->set_callable( $this->fields_action, form_id );
	}
}

// Register the custom loader
underpin()->loaders()->add('integrations',[
    'abstraction_class' => 'Integration',
    'default_factory'     => 'Integration_Instance'
]);

// Register Ninja Forms Integration
underpin()->integrations()->add('ninja_forms', [
  'get_form_action' => function( $form_id ){
    return Ninja_Forms()->form( 1 )->get();
  }
 /* Plus all the other callable actions */
]);

// Register Gravity Forms Integration
underpin()->integrations()->add('gravity_forms', [
  'get_form_action' => function( $form_id ){
    return GFAPI::get_form( $form_id );
  },
 /* Plus all the other callable actions */
]);

$ninja_forms_integration = underpin()->integrations()->get('ninja_forms'); //Do things with the Ninja Forms plugin
$gravity_forms_integration = underpin()->integrations()->get('gravity_forms'); //Do Things with the Gravity Forms plugin

// Gets the form using Ninja Forms.
$ninja_forms_integration->get_form();

// Gets the form using Gravity Forms.
$gravity_forms_integration->get_form();

How Could this be Improved?

This is a common scenario - building a third party plugin that needs to integrate with multiple, similar plugins. It would be nice if Underpin had some libraries pre-built to make it possible to work with these systems directly. So take all of the example code above, modify it to work as an integration, and make these work as loaders. This integration could be loaded in using composer, perhaps something like composer require underpin/forms-integration, which would contain all of the information needed to work with any forms plugin that is registered in the system. So, as long as a developer sticks to the methods that are included in Underpin's integration class, their plugin should be able to be compatible with all of the plugins that work with the integration for free.

This would allow developers to integrate with several plugins at one time, and also prevent common pitfalls with coupling their code directly with a forms plugin directly.

This could also make it possible for theme developers to integrate with more plugins without re-writing a bunch of code in the process.

Here's a few possible patterns:

// Get the integration class to work with the plugin.
// The intent is to make it so that this class would replace the need to use any direct functions or methods inside any forms plugin
$form_plugin = underpin()->integrations()->forms()->get( 'ninja_forms' );

$form_plugin->submit();

$form_plugin->get_fields();

Since this is technically a loader registry, you could also query registered integrations, and run something on each:

// Query integrations
$integrations = underpin()->integrations()->forms()->find([
  'features__in' => 'stored_submissions', // Only get form plugins that actually store submissions
]);

foreach( $integrations as $integration ){
    if($integration->is_active()){
      // Do something with the active integration
    }
}
class Integrations extends Loader_Registry{
  // Set up integrations as a loader registry. This would be built into Underpin, and be accessible via integrations()
  // It would probably have a __call method similar to the base Underpin class.
}

abstract class Integration{
  abstract public function is_active();
}

// Create the integration class
abstract class Form_Integration extends Integration{
    use Feature_Extension;

    abstract public function get_form( $form_id );

    abstract public function submit( $form_id, $data );

    abstract public function fields( $form_id );
}

class Form_Integration_Instance extends Form_Integration{
    	use Instance_Setter;

	protected $get_form_action;

	/**
	 * Block_Instance constructor.
	 *
	 * @param array $args Overrides to default args in the object
	 */
	public function __construct( array $args = [] ) {
		$this->set_values( $args );

		parent::__construct();
	}

	public function get_form( $form_id ){
		return $this->set_callable( $this->get_form_action, form_id );
	}

 	 abstract function submit( $form_id, $data ){
		return $this->set_callable( $this->submit_action, form_id, $data );
	}

	abstract function fields( $form_id ){
		return $this->set_callable( $this->fields_action, form_id );
	}
}

// Register the custom loader
underpin()->integrations()->add('forms',[
    'abstraction_class' => 'Form_Integration',
    'default_factory'     => 'Form_Integration_Instance'
]);

// Register Ninja Forms Integration
underpin()->integrations()->forms()->add('ninja_forms', [
  'get_form_action' => function( $form_id ){
    return Ninja_Forms()->form( $form_id )->get();
  }
 /* Plus all the other callable actions */
]);

// Register Gravity Forms Integration
underpin()->integrations()->forms()->add('gravity_forms', [
  'get_form_action' => function( $form_id ){
    return GFAPI::get_form( $form_id );
  },
 /* Plus all the other callable actions */
]);

$ninja_forms_integration = underpin()->integrations()->forms()->get('ninja_forms'); //Do things with the Ninja Forms plugin
$gravity_forms_integration = underpin()->integrations()->forms()->get('gravity_forms'); //Do Things with the Gravity Forms plugin

// Submits the form using Ninja Forms.
$ninja_forms_integration->submit( 1, ['key' => 'value'] );

// Submits the form using Gravity Forms.
$gravity_forms_integration->submit( 1, ['key' => 'value'] );

Now with the example above, let's say you wanted to automatically submit a form when something else happens on your site. You don't actually care what forms plugin is used, you just want the data to be submitted.

// Helper function to get the current forms integration
function get_forms_integration(){
    $current_integration = underpin()->options()->get( 'integration' )->get();

    $integration = underpin()->integrations()->forms()->get( $current_integration );

    if( is_wp_error( $integration ) ){
        underpin()->logger()->log_wp_error( $integration );
    }

    return $integration;
}

// Now a form submission will occur, regardless of what forms plugin was used.
add_action( 'my_super_cool_custom_hook', function( $data ){
    if(!is_wp_error(get_forms_integration()){
        get_forms_integration()->submit( 1, $data );
    }
} );

Other potential integrations that could work with this:

  1. E-Commerce plugins (EDD, WooCommerce, LifterLMS, LearnDash)
  2. Affiliate Plugins (AffiliateWP, SliceWP)
  3. Forms Plugins (Ninja Forms, Gravity Forms, Formidable Forms)
  4. SEO Plugins (Yoast, All in One SEO)
  5. Calendar Plugins (The Events Calendar, Sugar Calendar)
  6. LMS Plugins (LearnDash, LifterLMS)

I'm sure there's many more.

So What Needs Changed?

  1. Underpin needs to support a way to create, and register integrations. This pattern is already well-established in other parts of Underpin, such as loaders, so this is pretty straightforward.
  2. The example above only talks about working with the form, but what happens if we want to do something like get_forms_integration()->get_fields()? The resulting array of fields needs to be normalized in a standard Field object, so that the signature doesn't change between plugins. Something like get_forms_integration()->get_fields()->get_value() should work for every single type of forms plugin. Underpin might need to have some kind of abstraction to facilitate this.
  3. We need to be able to call the integrations registry by calling plugin_name()->integrations(), and the Integrations class should extend Loader_Registry so we can use helpers like find and filter.
  4. There needs to be a way to hook into the integrations loader and register integrations via a composer package. This may not need any additional code - it just depends on if we can hook into integrations in the same way we currently hook into loaders. Example.
  5. There needs to be a way to filter out integrations based on what they can, and cannot support. For example, not all forms plugins actually store the form submission data in the database. This should be stored as a hard variable, not a function, so the filter and find methods can query against it.
  6. It would be nice if it were possible to filter against which integration plugins are active, as well. This probably needs to run inside of a methods is_active, but it would be useful if the system knew that when active is added to filter/find that it would filter those plugins out.

This implementation probably needs to be coupled with a pre-built integration. This will help us figure out the nuance of working with a system like this, and also give us a concrete example of how to use/extend integrations.

Add sane defaults to taxonomies and post types

Registering custom post types and taxonomies each have a lot of arguments that could have a more-sane defaults. For example, there are several labels that need manually set, and most of them could be automatically set with a little logic to tell these CPTs to simply concatenate a singular, or plural version of the word.

Upgrade export class to support different formats

Currently, the export method simply dumps Underpin's data as HTML. It would be useful if it were possible to export this in different formats, such as JSON, PHP, or something custom.

This could make it easier to automatically build documentation, and could also be useful for support purposes, like a system info file.

Template Trait cannot be used on classes that also have a non-static subject trait

This is because With_Static_Subject and With_Subject each have methods that are named identically. This causes these methods to be overridden by one, or the other, and cause unexpected behavior.

The only obvious solution is to change the name of the methods in either With_Static_Subject or With_Subject to make it possible to have both in a single class.

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.