Giter Club home page Giter Club logo

tukio's Introduction

Tukio

Latest Version on Packagist Software License Total Downloads

Tukio is a complete and robust implementation of the PSR-14 Event Dispatcher specification. It supports normal and debug Event Dispatchers, both runtime and compiled Providers, complex ordering of Listeners, and attribute-based registration on PHP 8.

"Tukio" is the Swahili word for "Event".

Install

Via Composer

$ composer require crell/tukio

PSR-14 Usage

PSR-14 consists of two key components: A Dispatcher and a Provider. A Dispatcher is what a client library will compose and then pass Events objects to. A Provider is what a framework will give to a Dispatcher to match the Event to Listeners. Tukio includes multiple implementations of both.

The general structure of using any PSR-14-compliant implementation is:

$provider = new SomeProvider();
// Do some sort of setup on $provider so that it knows what Listeners it should match to what Events.
// This could be pretty much anything depending on the implementation.

$dispatcher = new SomeDispatcher($provider);

// Pass $dispatcher to some client code somewhere, and then:

$thingHappened = new ThingHappened($thing);

$dispatcher->dispatch($thingHappened);

Note that dispatch() will return $thingHappened as well, so if Listeners are expected to add data to it then you can chain a method call on it if desired:

$reactions = $dispatcher->dispatch(new ThingHappened($thing))->getReactions();

In practice most of that will be handled through a Dependency Injection Container, but there's no requirement that it do so.

Dispatchers

Tukio includes two Dispatchers: Dispatcher is the standard implementation, which should be used 95% of the time. It can optionally take a PSR-3 Logger, in which case it will log a warning on any exception thrown from a Listener.

The second is DebugEventDispatcher, which like the names says is useful for debugging. It logs every Event that is passed and then delegates to a composed dispatcher (such as Dispatcher) to actually dispatch the Event.

For example:

use Crell\Tukio\Dispatcher;
use Crell\Tukio\DebugEventDispatcher;

$provider = new SomeProvider();

$logger = new SomeLogger();

$dispatcher = new Dispatcher($provider, $logger);

$debugDispatcher = new DebugEventDispatcher($dispatcher, $logger);

// Now pass $debugDispatcher around and use it like any other dispatcher.

Providers

Tukio includes multiple options for building Providers, and it's encouraged that you combine them with the generic ones included in the fig/event-dispatcher-util library provided by the FIG.

Which Provider or Providers you use depends on your use case. All of them are valid for certain use cases.

OrderedListenerProvider

As the name implies, OrderedListenerProvider is all about ordering. Users can explicitly register Listeners on it that will be matched against an Event based on its type.

If no order is specified, then the order that Listeners will be returned is undefined (although in practice order-added is what it will most likely be). That makes the degenerate case super-easy:

use Crell\Tukio\OrderedListenerProvider;

class StuffHappened {}

class SpecificStuffHappened extends StuffHappened {}

function handleStuff(StuffHappened $stuff) { ... }


$provider = new OrderedListenerProvider();

$provider->addListener(function(SpecificStuffHappened) {
    // ...  
});

$provider->addListener('handleStuff');

That adds two Listeners to the Provider; one anonymous function and one named function. The anonymous function will be called for any SpecificStuffHappened event. The named function will be called for any StuffHappened or SpecificStuffHappened event. And the user doesn't really care which one happens first (which is the typical case).

Ordering listeners

However, the user can also be picky about the order in which Listeners will fire. The most common approach for that today is via an integral "priority", and Tukio offers that:

use Crell\Tukio\OrderedListenerProvider;

$provider = new OrderedListenerProvider();

$provider->addListener(function(SpecificStuffHappened) {
    // ...  
}, 10);

$provider->addListener('handleStuff', 20);

Now, the named function Listener will get called before the anonymous function does. (Higher priority number comes first, and negative numbers are totally legal.) If two listeners have the same priority then their order relative to each other is undefined.

Sometimes, though, you may not know the priority of another Listener, but it's important your Listener happen before or after it. For that we need to add a new concept: IDs. Every Listener has an ID, which can be provided when the Listener is added or will be auto-generated if not. The auto-generated value is predictable (the name of a function, the class-and-method of an object method, etc.), so in most cases it's not necessary to read the return value of addListener() although that is slightly more robust.

use Crell\Tukio\OrderedListenerProvider;

$provider = new OrderedListenerProvider();

// The ID will be "handleStuff", unless there is such an ID already,
//in which case it would be "handleStuff-1" or similar.
$id = $provider->addListener('handleStuff');

// This Listener will get called before handleStuff does. If you want to specify an ID
// you can, since anonymous functions just get a random string as their generated ID.
$provider->addListenerBefore($id, function(SpecificStuffHappened) {
    // ...  
}, 'my_specifics');

Here, the priority of handleStuff is undefined; the user doesn't care when it gets called. However, the anonymous function, if it should get called at all, will always get called after handleStuff does. It's possible that some other Listener could also be called in between the two, but one will always happen after the other.

The full API for those three methods is:

public function addListener(callable $listener, $priority = 0, string $id = null, string $type = null): string;

public function addListenerBefore(string $before, callable $listener, string $id = null, string $type = null): string;

public function addListenerAfter(string $after, callable $listener, string $id = null, string $type = null): string;

All three can register any callable as a Listener and return an ID. If desired the $type parameter allows a user to specify the Event type that the Listener is for if different than the type declaration in the function. For example, if the Listener doesn't have a type declaration or should only apply to some parent class of what it's type declaration is. (That's a rare edge case, which is why it's the last parameter.)

Service Listeners

Often, though, Listeners are themselves methods of objects that should not be instantiated until and unless needed. That's exactly what a Dependency Injection Container allows, and OrderedListenerProvider fully supports those, called "Service Listeners". They work almost exactly the same, except you specify a service and method name:

public function addListenerService(string $serviceName, string $methodName, string $type, $priority = 0, string $id = null): string;

public function addListenerServiceBefore(string $before, string $serviceName, string $methodName, string $type, string $id = null): string;

public function addListenerServiceAfter(string $after, string $serviceName, string $methodName, string $type, string $id = null) : string;

All three take a service name and method name to identify a Listener. However, since the callable itself doesn't exist yet it's not possible to derive the Event it should listen to through reflection. It must be specified explicitly. All three also return an ID, so the same "priority-or-before/after" functionality is supported.

The services themselves can be from any PSR-11-compatible Container.

use Crell\Tukio\OrderedListenerProvider;

$container = new SomePsr11Container();
// Configure the container somehow.

$provider = new OrderedListenerProvider($container);

$provider->addListenerService('some_service', 'methodA', ThingHappened::class);

$id = $provider->addListenerServiceBefore('some_service-methodA', 'some_service', 'methodB', SpecificThingHappened::class);

$provider->addListenerServiceAfter($id, 'some_other_service', 'methodC', SpecificThingHappened::class);

In this example, we assume that $container has two services defined: some_service and some_other_service. (Creative, I know.) We then register three Listeners: Two of them are methods on some_service, the other on some_other_service. Both services will be requested from the container as needed, so won't be instantiated until the Listener method is about to be called.

Of note, the methodB Listener is referencing the methodA listener by an explict ID. The generated ID is as noted predictable, so in most cases you don't need to use the return value. The return value is the more robust and reliable option, though, as if the requested ID is already in-use a new one will be generated.

Subscribers

As in the last example, it's quite common to have multiple Listeners in a single service. In fact, it's common to have a service that is nothing but listeners. Tukio calls those "Subscribers" (a name openly and unashamedly borrowed from Symfony), and has specific support for them.

The basic case works like this:

use Crell\Tukio\OrderedListenerProvider;

class Subscriber
{
    public function onThingsHappening(ThingHappened $event) : void { ... }

    public function onSpecialEvent(SpecificThingHappened $event) : void { ... }
}

$container = new SomePsr11Container();
// Configure the container so that the service 'listeners' is an instance of Subscriber above.

$provider = new OrderedListenerProvider($container);


$provider->addSubscriber(Subscriber::class, 'listeners');

That's it! Because we don't know what the class of the service is it will need to be specified explicitly, but the rest is automatic. Any public method whose name begins with "on" will be registered as a listener, and the Event type it is for will be derived from reflection, while the rest of the class is ignored. There is no limit to how many listeners can be added this way. The method names can be anything that makes sense in context, so make it descriptive. And because the service is pulled on-demand from the container it will only be instantiated once, and not before it's needed. That's the ideal.

Sometimes, though, you want to order the Listeners in the Subscriber, or need to use a different naming convention for the Listener method. For that case, your Subscriber class can implement an extra interface that allows for manual registration:

use Crell\Tukio\OrderedListenerProvider;
use Crell\Tukio\SubscriberInterface;

class Subscriber implements SubscriberInterface
{
    public function onThingsHappening(ThingHappened $event) : void { ... }

    public function onSpecialEvent(SpecificThingHappened $event) : void { ... }

    public function somethingElse(ThingHappened $event) : void { ... }

    public static function registerListeners(ListenerProxy $proxy) : void
    {
        $id = $proxy->addListener('somethingElse', 10);
        $proxy->addListenerAfter($id, 'onSpecialEvent');
    }
}

$container = new SomePsr11Container();
// Configure the container so that the service 'listeners' is an instance of Subscriber above.

$provider = new OrderedListenerProvider($container);

$provider->addSubscriber(Subscriber::class, 'listeners');

As before, onThingsHappen() will be registered automatically. However, somethingElse() will also be registered as a Listener with a priority of 10, and onSpecialEvent() will be registered to fire after it.

In practice, using Subscribers is the most robust way to register Listeners in a production system and so is the recommended approach. However, all approaches have their uses and can be used as desired.

If you are using PHP 8.0 or later, you can use attributes to register your subscriber methods instead. That is preferred, in fact. See the section below.

Compiled Provider

All of that registration and ordering logic is powerful, and it's surprisingly fast in practice. What's even faster, though, is not having to re-register on every request. For that, Tukio offers a compiled provider option.

The compiled provider comes in three parts: ProviderBuilder, ProviderCompiler, and a generated provider class. ProviderBuilder, as the name implies, is an object that allows you to build up a set of Listeners that will make up a Provider. They work exactly the same as on OrderedListenerProvider, and in fact it exposes the same OrderedProviderInterface.

ProviderCompiler then takes a builder object and writes a new PHP class to a provided stream (presumably a file on disk) that matches the definitions in the builder. That built Provider is fixed; it cannot be modified and no new Listeners can be added to it, but all of the ordering and sorting has already been done, making it notably faster (to say nothing of skipping the registration process itself).

Let's see it in action:

use Crell\Tukio\ProviderBuilder;
use Crell\Tukio\ProviderCompiler;

$builder = new ProviderBuilder();

$builder->addListener('listenerA', 100);
$builder->addListenerAfter('listenerA', 'listenerB');
$builder->addListener([Listen::class, 'listen']);
$builder->addListenerService('listeners', 'listen', CollectingEvent::class);
$builder->addSubscriber('subscriber', Subscriber::class);

$compiler = new ProviderCompiler();

// Write the generated compiler out to a file.
$filename = 'MyCompiledProvider.php';
$out = fopen($filename, 'w');

// Here's the magic:
$compiler->compile($builder, $out, 'MyCompiledProvider', '\\Name\\Space\\Of\\My\\App');

fclose($out);

$builder can do anything that OrderedListenerProvider can do, except that it only supports statically-defined Listeners. That means it does not support anonymous functions or methods of an object, but it will still handle functions, static methods, services, and subscribers just fine. In practice when using a compiled container you will most likely want to use almost entirely service listeners and subscribers, since you'll most likely be using it with a container.

That gives you a file on disk named MyCompiledProvider.php, which contains Name\Space\Of\My\App\MyCompiledProvider. (Name it something logical for you.) At runtime, then, do this:

// Configure the container such that it has a service "listeners"
// and another named "subscriber".

$container = new Psr11Container();
$container->addService('D', new ListenService());

include('MyCompiledProvider.php');

$provider = new Name\Space\Of\My\App\MyCompiledProvider($container);

And boom! $provider is now a fully functional Provider you can pass to a Dispatcher. It will work just like any other, but faster.

But what if you want to have most of your listeners pre-registered, but have some that you add conditionally at runtime? Have a look at the FIG's AggregateProvider, and combine your compiled Provider with an instance of OrderedListenerProvider.

Compiler optimization

The ProviderBuilder has one other trick. If you specify one or more events via the optimizeEvent($class) method, then the compiler will pre-compute what listeners apply to it based on its type, including its parent classes and interfaces. The result is a constant-time simple array lookup for those events, also known as "virtually instantaneous."

use Crell\Tukio\ProviderBuilder;
use Crell\Tukio\ProviderCompiler;

$builder = new ProviderBuilder();

$builder->addListener('listenerA', 100);
$builder->addListenerAfter('listenerA', 'listenerB');
$builder->addListener([Listen::class, 'listen']);
$builder->addListenerService('listeners', 'listen', CollectingEvent::class);
$builder->addSubscriber('subscriber', Subscriber::class);

// Here's where you specify what events you know you will have.
// Returning the listeners for these events will be near instant.
$builder->optimizeEvent(EvenOne::class);
$builder->optimizeEvent(EvenTwo::class);

$compiler = new ProviderCompiler();

// Write the generated compiler out to a file.
$filename = 'MyCompiledProvider.php';
$out = fopen($filename, 'w');

// Here's the magic:
$compiler->compile($builder, $out, 'MyCompiledProvider', '\\Name\\Space\\Of\\My\\App');

fclose($out);

Attribute-based registration

If you are using PHP 8.0 or later, Tukio fully supports attributes as a means of registration for both OrderedListenerProvider and ProviderBuilder. There are four relevant attributes: Listener, ListenerPriority, ListenerBefore, and ListenerAfter. All can be used with sequential parameters or named parameters. In most cases, named parameters will be more self-documenting. All attributes are valid only on functions and methods.

  • Listener declares a callable a listener and optionally sets the id and type: #[Listener(id: 'a_listener', type: SomeClass`)].
  • ListenerPriority has a required priority parameter, and optional id and type: #[ListenerPriority(5)]or#[ListeenPriority(priority: 3, id: "a_listener")]`.
  • ListenerBefore has a required before parameter, and optional id and type: #[ListenerBefore('other_listener')]or#[ListenerBefore(before: 'other_listener', id: "a_listener")]`.
  • ListenerAfter has a required after parameter, and optional id and type: #[ListenerAfter('other_listener')]or#[ListenerAfter(after: 'other_listener', id: "a_listener")]`.

Each attribute matches a corresponding method, and the values in the attribute will be used as if they were passed directly to the addListener*() method. If a value is passed directly to addListener() and specified in the attribute, the directly-passed value takes precedence.

If you call addListenerBefore()/addListenerAfter(), the before/after/priority attribute parameter is ignored in favor of the method's standard behavior. That is, addListenerBefore(), if called with a function that has a #[ListenerAfter] attribute, will still add the listener "before" the specified other listener.

There are two common use cases for attributes: One, using just #[Listener] to define a custom id or type and then calling the appropriate add* method as normal. The other is on Subscribers, where attributes completely eliminate the need for the registerListeners() method. If you're on PHP 8.0, please use attributes instead of registerListeners(). It's not deprecated yet, but it may end up deprecated sometime in the future.

For example:

class SomeSubscriber
{
    // Registers, with a custom ID.
    #[Listener(id: 'a')]
    public function onA(CollectingEvent $event) : void
    {
        $event->add('A');
    }

    // Registers, with a custom priority.
    #[ListenerPriority(priority: 5)]
    public function onB(CollectingEvent $event) : void
    {
        $event->add('B');
    }

    // Registers, before listener "a" above.
    #[ListenerBefore(before: 'a')]
    public function onC(CollectingEvent $event) : void
    {
        $event->add('C');
    }

    // Registers, after listener "a" above.
    #[ListenerAfter(after: 'a')]
    public function onD(CollectingEvent $event) : void
    {
        $event->add('D');
    }

    // This still self-registers because of the name.
    public function onE(CollectingEvent $event) : void
    {
        $event->add('E');
    }

    // Registers, with a given priority despite its non-standard name.
    #[ListenerPriority(priority: -5)]
    public function notNormalName(CollectingEvent $event) : void
    {
        $event->add('F');
    }

    // No attribute, non-standard name, this method is not registered.
    public function ignoredMethodThatDoesNothing() : void
    {
        throw new \Exception('What are you doing here?');
    }
}

CallbackProvider

The third option Tukio provides is a CallbackProvider, which takes an entirely different approach. In this case, the Provider works only on events that have a CallbackEventInterface. The use case is for Events that are carrying some other object, which itself has methods on it that should be called at certain times. Think lifecycle callbacks for a domain object, for example.

To see it in action, we'll use an example straight out of Tukio's test suite:

use Crell\Tukio\CallbackEventInterface;
use Crell\Tukio\CallbackProvider;

class LifecycleEvent implements CallbackEventInterface
{
    protected $entity;

    public function __construct(FakeEntity $entity)
    {
        $this->entity = $entity;
    }

    public function getSubject() : object
    {
        return $this->entity;
    }
}

class LoadEvent extends LifecycleEvent {}

class SaveEvent extends LifecycleEvent {}

class FakeEntity
{

    public function load(LoadEvent $event) : void { ... }

    public function save(SaveEvent $event) : void { ... }

    public function stuff(StuffEvent $event) : void { ... }

    public function all(LifecycleEvent $event) : void { ... }
}

$provider = new CallbackProvider();

$entity = new FakeEntity();

$provider->addCallbackMethod(LoadEvent::class, 'load');
$provider->addCallbackMethod(SaveEvent::class, 'save');
$provider->addCallbackMethod(LifecycleEvent::class, 'all');

$event = new LoadEvent($entity);

$provider->getListenersForEvent($event);

In this example, the provider is configured not with Listeners but with method names that correspond to Events. Those methods are methods on the "subject" object. The Provider will now return callables for [$entity, 'load'] and [$entity, 'all'] when called with a LoadEvent. That allows a domain object itself to have Listeners on it that will get called at the appropriate time.

Change log

Please see CHANGELOG for more information on what has changed recently.

Testing

$ composer test

Contributing

Please see CONTRIBUTING and CODE_OF_CONDUCT for details.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

Credits

License

The Lesser GPL version 3 or later. Please see License File for more information.

tukio's People

Contributors

crell avatar jbboehr avatar martinssipenko avatar niflthaena avatar romm 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tukio's Issues

Missing Psr\EventDispatcher\EventInterface ?

Remove support for repeating attributes?

This is an informational request. For Tukio v2, I am trying to clean up some legacy design. One of the things that I don't think I thought through very well is that attribute-based registration supports specifying multiple attributes on the same listener. I... don't really see how that's useful. If anything, it's just making the logic for addListener() more complicated and confusing. Like, if there are attributes defined and a priority is passed to addListener(), what happens? What overrides what?

So I want to remove it.

Before I do so, does anyone rely on that functionality, and is not able to get the same behavior another way? I really cannot think of a use case, but if you can, please share. If no one has any, I will most likely remove that ability to make the code simpler.

PHP 8.1 support

Hello,

I've started work on adding PHP 8.1 support in #17, but it seems that dropping 7.2 would be required to do so. Would you be ok with that?

Use Doctrine annotations for registration

Leaving this as a note to myself...

To effectively register subscribers and provide alternate IDs we probably need to go ahead and just use Doctrine annotations. While it might be fun to write an annotation parser it's probably out of scope, but it's another reason that PHP needs native annotations.

Subscriber method registration bug

Detailed description

Subscriber registration, based on the documentation, sounds very useful and easy to integrate. Its implementation is decidedly less so.
Specifically, the documentation says that any method on a Subscriber whose name begins with 'on' is registered. However, the code is written such that any method containing the text 'on' is caught. (for example, "encodeJson".)
Per src/OrderedListenerProvider.php:131:
if (!in_array($methodName, $proxy->getRegisteredMethods()) && strpos($methodName, 'on') !== false) {

To make matters worse, if the first parameter of any such method does not have a typehint, auto-registration immediately throws a fatal error, rather than simply failing to register.
Per src/OrderedListenerProvider.php:133:
$type = $params[0]->getType()->getName();
getType() returns null, getName() throws a 'member function on null' fatal error.

Context

When integrating with an existing codebase, it would be nice to be able to use some convention (or annotation) to turn manager classes into subscribers. Instead, auto-registration has the above issue, leaving manual registration as the alternative, or refactoring the classes that wish to subscribe to avoid placing an 'n' after an 'o'.

Test concerns

The current automated tests have methods that are not to be registered, but coincidentally these methods do not include the text 'on' anywhere in their name. A test case such "public function doNotAttach_onRegistration" would test for this, and have caught the issue.

Possible implementation

A potential fix would be simply comparing strpos($methodName, 'on') === 0.

Remove Subscriber interface and rely on attributes?

For Tukio v2, I am considering removing the Subscriber interface. It's a bit fiddly, and in practice there is extremely little that you cannot do just as well with attributes now. Certainly any static registration is more easily done with attributes. And if you have especially complex logic for registering listeners, it's probably better to do it manually yourself anyway.

If I do remove it, I'll probably add one last release of the 1.x series that marks it deprecated, and possibly a few other things as well along the way.

If I hear nothing, I will consider it safe to remove. Speak now or forever hold your peace. :-)

Figure out how to use Tukio as a replacement for a legacy event system

Hello @Crell,

this is not a real issue but more of a help request to better understand the concept of Listeners, Subscribers, Providers, Dispatcher and how they interact in Tukio.

Feel free to just close the issue without any comment, if the issue tracker should not be used for such requests.


After reading your PEAKD articles about PSR-14 and finding out about Tukio, I want to "renovate" the event system in our legacy application. It should follow PSR-14 by using Tukio.

After playing around with Tukio in a test project, I was really happy and thought I could swap out the existing event system easily.

But after further consideration, I have a hard time figuring out if I can/should use Listeners or Subscribers and if multiple Providers are required.


Here is a simplified version of how the event system works at the moment:

The current event system is part of a plugin-based application. Every plugin provides an XML file that defines which event should be handled by which class and which static method. It is also possible to specify an event priority/order.

Now on every request the "core" reads all XML files and registers all event listeners.

To fire an event, a method inside the "core" can be called. The "core" then calls all corresponding event listeners.


To replace this plugin-based event system with Tukio, I came up with the following approaches:

  1. Every plugin has one or more event providers that have to implement the ListenerProviderInterface. The XML file then contains a list of those providers. The "core" then uses an AggregateProvider to aggregate all providers into one. The AggregateProvider is then passed into a Dispatcher inside the "core" that can be used to fire an event.
  2. Every plugin has one or more Subscribers. The XML file then contains a list of those subscribers. The "core" then uses an OrderedListenerProvider to add all Subscribers. The OrderedListenerProvider is then passed into a Dispatcher inside the "core" that can be used to fire an event.

But I have the following problems with those approaches:

  1. This won't allow me to prioritize/order events (?). I also do not want that plugin developers have to know the ListenerProviderInterface and have to come up with their own implementation.
  2. Using Subscribers requires a PSR-11 container. Unfortunately the application does not use dependency injection and/or PSR-11 and implementing it would be a lot of effort.

Now I feel like I'm missing something, since both approaches aren't ideal.
I was imaging a solution where every plugin defines their event listeners as (static) methods by simply using attribute-based registration. The "core" then automagically finds those subscribers and adds them to a central OrderedListenerProvider. That provider is then passed into a global Dispatcher that can be used to fire events.

Is it true that dependency injection and/or PSR-11 containers would be required for such a solution?
Is this even an "ideal" solution or is my reasoning affected by the existing legacy concepts?
How would you approach this problem?

Thanks for your time and help in advance.
I would greatly appreciate any advice.

Best regards,
Jan

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.