{"id":20012338,"url":"https://github.com/archtechx/airwire","last_synced_at":"2025-11-07T02:04:47.565Z","repository":{"id":44653807,"uuid":"369230221","full_name":"archtechx/airwire","owner":"archtechx","description":"A lightweight full-stack component layer that doesn't dictate your front-end framework","archived":false,"fork":false,"pushed_at":"2022-02-08T12:57:11.000Z","size":122,"stargazers_count":200,"open_issues_count":12,"forks_count":9,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-05-19T22:11:24.536Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://archte.ch/blog/introducing-airwire","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/archtechx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"stancl"}},"created_at":"2021-05-20T14:08:47.000Z","updated_at":"2025-04-15T17:35:55.000Z","dependencies_parsed_at":"2022-09-19T18:42:13.416Z","dependency_job_id":null,"html_url":"https://github.com/archtechx/airwire","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/archtechx/airwire","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Fairwire","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Fairwire/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Fairwire/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Fairwire/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/archtechx","download_url":"https://codeload.github.com/archtechx/airwire/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Fairwire/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263427308,"owners_count":23464842,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-13T07:29:48.143Z","updated_at":"2025-11-07T02:04:47.537Z","avatar_url":"https://github.com/archtechx.png","language":"PHP","funding_links":["https://github.com/sponsors/stancl"],"categories":[],"sub_categories":[],"readme":"\u003e Note: **Development is currently paused**. It will be resumed after we launch [Lean Admin](https://lean-admin.dev) this year.\n\n# Airwire\n\n*A lightweight full-stack component layer that doesn't dictate your front-end framework*\n\n[Demo](https://github.com/archtechx/airwire-demo)\n\n## Introduction\n\nAirwire is a thin layer between your Laravel code and your JavaScript.\n\nIt lets you write Livewire-style OOP components like this:\n\n```php\nclass CreateUser extends Component\n{\n    #[Wired]\n    public string $name = '';\n\n    #[Wired]\n    public string $email = '';\n\n    #[Wired]\n    public string $password = '';\n\n    #[Wired]\n    public string $password_confirmation = '';\n\n    public function rules()\n    {\n        return [\n            'name' =\u003e ['required', 'min:5', 'max:25', 'unique:users'],\n            'email' =\u003e ['required', 'unique:users'],\n            'password' =\u003e ['required', 'min:8', 'confirmed'],\n        ];\n    }\n\n    #[Wired]\n    public function submit(): User\n    {\n        $user = User::create($this-\u003evalidated());\n\n        $this-\u003emeta('notification', __('users.created', ['id' =\u003e $user-\u003eid, 'name' =\u003e $user-\u003ename]));\n\n        $this-\u003ereset();\n\n        return $user;\n    }\n}\n```\n\nThen, it generates a TypeScript definition like this:\n\n```ts\ninterface CreateUser {\n    name: string;\n    email: string;\n    password: string;\n    password_confirmation: string;\n    submit(): AirwirePromise\u003cUser\u003e;\n    errors: { ... }\n\n    // ...\n}\n```\n\nAnd Airwire will wire the two parts together. It's up to you what frontend framework you use (if any), Airwire will simply forward calls and sync state between the frontend and the backend.\n\nThe most basic use of Airwire would look like this:\n\n```ts\nlet component = Airwire.component('create-user')\n\nconsole.log(component.name); // your IDE knows that this is a string\n\ncomponent.name = 'foo';\n\ncomponent.errors; // { name: ['The name must be at least 10 characters.'] }\n\n// No point in making three requests here, so let's defer the changes\ncomponent.deferred.name = 'foobar';\ncomponent.deferred.password = 'secret123';\ncomponent.deferred.password_confirmation = 'secret123';\n\n// Watch all received responses\ncomponent.watch(response =\u003e {\n    if (response.metadata.notification) {\n        alert(response.metadata.notification)\n    }\n})\n\ncomponent.submit().then(user =\u003e {\n    // TS knows the exact data structure of 'user'\n    console.log(user.created_at);\n})\n```\n\n## Installation\n\n*Laravel 8 and PHP 8 are needed.*\n\nFirst install the package via composer:\n```\ncomposer require archtechx/airwire\n```\n\nThen go to your `webpack.mix.js` and register the watcher plugin. It will refresh the TypeScript definitions whenever you make a change to PHP code:\n\n```js\nmix.webpackConfig({\n    plugins: [\n        new (require('./vendor/archtechx/airwire/resources/js/AirwireWatcher'))(require('chokidar')),\n    ],\n})\n```\n\nNext, generate the initial TS files:\n\n```\nphp artisan airwire:generate\n```\n\nThis will create `airwire.ts` and `airwired.d.ts`. Open your `app.ts` and import the former:\n\n```ts\nimport Airwire from './airwire'\n```\n\nIf you have an `app.js` file instead of an `app.ts` file, change the file suffix and update your `webpack.mix.js` file:\n\n```diff\n- mix.js('resources/js/app.js', 'public/js')\n+ mix.ts('resources/js/app.ts', 'public/js')\n```\n\nIf you're using TypeScript for the first time, you'll also need a `tsconfig.json` file in the the root of your project. You can use this one to get started:\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"strict\": true,\n    \"module\": \"es2015\",\n    \"moduleResolution\": \"node\",\n    \"experimentalDecorators\": true,\n    \"sourceMap\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"resources/js/**/*\"]\n}\n```\n\nAnd that's all! Airwire is fully installed.\n\n## PHP components\n\n### Creating components\n\nTo create a component run the `php artisan airwire:component` command.\n\n```\nphp artisan airwire:component CreateUser\n```\n\nThe command in the example will create a file in `app/Airwire/CreateUser.php`.\n\nNext, register it in your AppServiceProvider:\n\n```php\n// boot()\n\nAirwire::component('create-user', CreateUser::class);\n```\n\n### Wired properties and methods\n\nComponent properties and methods will be shared with the frontend if they use the `#[Wired]` attribute (in contrast to Livewire, where `public` visibility is used for this).\n\nThis means that your components can use properties (even public) just fine, and they won't be shared with the frontend until you explicitly add this attribute.\n\n```php\nclass CreateTeam extends Component\n{\n    #[Wired]\n    public string $name; // Shared\n\n    public string $owner; // Not shared\n\n    public function hydrate()\n    {\n        $this-\u003eowner = auth()-\u003eid();\n    }\n}\n```\n\n### Lifecycle hooks\n\nAs showed in the example above, Airwire has useful lifecycle hooks:\n\n```php\npublic function hydrate()\n{\n    // Executed on each request, before any changes \u0026 calls are made\n}\n\npublic function dehydrate()\n{\n    // Executed when serving a response, before things like validation errors are serialized into array metadata\n}\n\npublic function updating(string $property, mixed $value): bool\n{\n    return false; // disallow this state change\n}\n\npublic function updatingFoo(mixed $value): bool\n{\n    return true; // allow this state change\n}\n\npublic function updated(string $property, mixed $value): void\n{\n    // execute side effects as a result of a state change\n}\n\npublic function updatedFoo(mixed $value): void\n{\n    // execute side effects as a result of a state change\n}\n\npublic function changed(array $changes): void\n{\n    // execute side effects $changes has a list of properties that were changed\n    // i.e. passed validation and updating() hooks\n}\n```\n\n### Validation\n\nAirwire components use **strict validation** by default. This means that no calls can be made if the provided data is invalid.\n\nTo disable strict validation, set this property to false:\n```php\npublic bool $strictValidation = false;\n```\n\nNote that disabling strict validation means that you're fully responsible for validating all incoming input before making any potentially dangerous calls, such as database queries.\n\n```php\npublic array $rules = [\n    'name' =\u003e ['required', 'string', 'max:100'],\n];\n\n// or ...\npublic function rules()\n{\n    return [ ... ];\n}\n\npublic function messages()\n{\n    return [ ... ];\n}\n\npublic function attributes()\n{\n    return [ ... ];\n}\n```\n\n### Custom types\n\nAirwire supports custom DTOs. Simply tell it how to decode (incoming requests) and encode (outgoing responses) the data:\n\n```php\nAirwire::typeTransformer(\n    type: MyDTO::class,\n    decode: fn (array $data) =\u003e new MyDTO($data['foo'], $data['abc']),\n    encode: fn (MyDTO $dto) =\u003e ['foo' =\u003e $dto-\u003efoo, 'abc' =\u003e $dto-\u003eabc],\n);\n```\n\nThis doesn't require changes to the DTO class, and it works with any classes that extend the class.\n\n### Models\n\nA type transformer for models is included by default. It uses the `toArray()` method to generate a JSON-friendly representation of the model (which means that things like `$hidden` are respected).\n\nIt supports converting received IDs to model instances:\n```php\n// received: '3'\npublic User $user;\n```\n\nConverting arrays/objects to unsaved instances:\n```php\n// received: ['name' =\u003e 'Try Airwire on a new project', 'priority' =\u003e 'highest']\npublic function addTask(Task $task)\n{\n    $task-\u003esave();\n}\n```\n\nConverting properties/return values to arrays:\n```php\npublic User $user;\n// response: {\"name\": \"John Doe\", \"email\": \"john@example.com\", ... }\n\npublic find(string $id): Response\n{\n    return User::find($id);\n}\n// same response as the property\n```\n\nIf you wish to have even more control over how the data should be encoded, on a property-by-property basis, you can add a `Decoded` attribute. This can be useful for returning the id of a model, even if a property holds its instance:\n```php\n#[Wired] #[Encode(method: 'getKey')]\npublic User $user; // returns '3'\n\n#[Wired] #[Encode(property: 'slug')]\npublic Post $post; // returns 'introducing-airwire'\n\n#[Wired] #[Encode(function: 'generateHashid')]\npublic Post $post; // returns the value of generateHashid($post)\n```\n\n### Default values\n\nYou can specify default values for properties that can't have them specified directly in the class:\n\n```php\n#[Wired(default: [])]\npublic Collection $results;\n```\n\nThese values will be part of the generated JS files, which means that components will have correct initial state even if they're initialized purely on the frontend, before making a single request to the server.\n\n### Readonly values\n\nProperties can also be readonly. This tells the frontend not to send them to the backend in request data.\n\nA good use case for readonly properties is data that's only written by the server, e.g. query results:\n\n```php\n// Search/Filter component\n\n#[Wired(readonly: true, default: [])]\npublic Collection $results;\n```\n\n### Mounting components\n\nComponents can have a `mount()` method, which returns initial state. This state is not accessible when the component is instantiated on the frontend (unlike default values of properties), so the component requests the data from the server.\n\nA good use case for `mount()` is `\u003cselect\u003e` options:\n\n```php\npublic function mount()\n{\n    return [\n        'users' =\u003e User::all()-\u003etoArray(),\n    ]\n}\n```\n\nMount data is often readonly, so the method supports returning values that will be added to the frontend component's readonly data:\n\n```php\npublic function mount()\n{\n    return [\n        'readonly' =\u003e [\n            'users' =\u003e User::all()-\u003etoArray(),\n        ],\n    ];\n}\n```\n\n### Metadata\n\nYou can also add metadata to Airwire responses:\n\n```php\npublic function save(User $user): User\n{\n    $this-\u003evalidate($user-\u003egetAttributes());\n\n    if ($user-\u003esave()) {\n        $this-\u003emetadata('The user was saved with an id of ' . $user-\u003eid);\n    } else {\n        throw Exception(\"The user couldn't be created.\");\n    }\n}\n```\n\nThis metadata will be accessible to response watchers which are documented in the next section.\n\n## Frontend\n\nAirwire provides several helpers on the frontend.\n\n### Global watcher\n\nAll responses can be watched on the frontend. This is useful for displaying notifications and rendering exceptions.\n\n```ts\n// Component-specific\ncomponent.watch(response =\u003e {\n    // ...\n});\n\n// Global\nAirwire.watch(response =\u003e {\n    // response.data\n\n    if (response.metadata.notification) {\n        notify(response.metadata.notification)\n    }\n\n    if (response.metadata.errors) {\n        notify('You entered invalid data.', { color: 'red' })\n    }\n}, exception =\u003e {\n    alert(exception)\n})\n```\n\n### Reactive helper\n\nAirwire lets you specify a helper for creating singleton proxies of components. They are used for integrating with frontend frameworks.\n\nFor example, integrating with Vue is as easy as:\n\n```ts\nimport { reactive } from 'vue'\n\nAirwire.reactive = reactive\n```\n\n### Integrating with Vue.js\n\nAs mentioned above, you can integrate Airwire with Vue using a single line of code.\n\nIf you'd also like a `this.$airwire` helper (to avoid having to use `window.Airwire`), you can use our Vue plugin. Here's how an example `app.ts` might look like:\n\n```ts\nimport Airwire from './airwire';\n\nimport { createApp, reactive } from 'vue';\n\ncreateApp(require('./components/Main.vue').default)\n    .use(Airwire.plugin('vue')(reactive))\n    .mount('#app')\n\ndeclare module 'vue' {\n    export interface ComponentCustomProperties {\n        $airwire: typeof window.Airwire\n    }\n}\n```\n\n```ts\ndata() {\n    return {\n        component: this.$airwire.component('create-user', {\n            name: 'John Doe',\n        }),\n    }\n},\n```\n\n### Integrating with Alpine.js\n\n\u003e Note: The Alpine integration hasn't been tested, but we *expect* it to work correctly. We'll be reimplementing the Vue demo in Alpine soon.\n\nAlpine doesn't have a `reactive()` helper like Vue, [so we created it](https://github.com/archtechx/alpine-reactive).\n\nThere's one caveat: it's not global, but rather component-specific. It works with a list of components to update when the data mutates.\n\nFor that reason, you'd need to pass the reactive helper inside the component:\n\n```html\n\u003cdiv x-data=\"{\n    component: Airwire.component('create-user', {\n        name: 'John Doe',\n    }, $reactive)\n}\"\u003e\u003c/div\u003e\n```\n\nTo simplify that, you may use our Airwire plugin which provides an `$airwire` helper:\n\n```html\n\u003cdiv x-data=\"{\n    component: $airwire('create-user', {\n        name: 'John Doe',\n    })\n}\"\u003e\u003c/div\u003e\n```\n\nTo use the plugin, use this call **before importing Alpine**:\n\n```\nAirwire.plugin('alpine')()\n```\n\n## Testing\n\nAirwire components are fully testable using fluent syntax:\n\n```php\n// Assertions against responses use send()\ntest('properties are shared only if they have the Wired attribute', function () {\n    expect(TestComponent::test()\n        -\u003estate(['foo' =\u003e 'abc', 'bar' =\u003e 'xyz'])\n        -\u003esend()\n        -\u003edata\n    )-\u003etoBe(['bar' =\u003e 'xyz']); // foo is not Wired\n});\n\n// Assertions against component state use hydrate()\ntest('properties are shared only if they have the Wired attribute', function () {\n    expect(TestComponent::test()\n        -\u003estate(['foo' =\u003e 'abc', 'bar' =\u003e 'xyz'])\n        -\u003ehydrate()-\u003ebar\n    )-\u003etoBe('xyz'); // foo is not Wired\n});\n```\n\nYou can look at the [package's tests](https://github.com/archtechx/airwire/blob/master/tests/Airwire/ValidationTest.php) to see real-world examples.\n\n## Protocol spec\n\nAirwire components aren't signed or fingerprinted in any way. They're completely stateless just like a REST API, which allows for instantiation from the frontend. This is in contrast to Livewire which doesn't allow any direct state changes — they all have to be \"approved\" and signed by the backend.\n\nThe best way to think about Airwire is simply an OOP wrapper around a REST API. Rather than writing low-level controllers and routes, you write expressive object-oriented components.\n\n### Request\n\n```json\n{\n    \"state\": {\n        \"foo\": \"abcdef\"\n    },\n    \"changes\": {\n        \"foo\": \"bar\"\n    },\n    \"calls\": {\n        \"save\": [\n            {\n                \"name\": \"Example task\",\n                \"priority\": \"highest\"\n            }\n        ]\n    }\n}\n```\n\n### Response\n\n```json\n{\n    \"data\": {\n        \"foo\": \"abcdef\"\n    },\n    \"metadata\": {\n        \"errors\": {\n            \"foo\": [\n                \"The name must be at least 10 characters.\"\n            ]\n        },\n        \"exceptions\": {\n            \"save\": \"Insufficient permissions.\"\n        }\n    }\n}\n```\n\n### State\n\nThe state refers to the old state, before any changes are made. The difference isn't big, since Airwire doesn't blindly trust the state, but it is separated from changes in the request.\n\nOne use of this are the `updating`, `updated`, and `changed` lifecycle hooks.\n\n### Changes\n\nIf the change is not allowed, Airwire will silently fail and simply exclude the change from the request.\n\n### Calls\n\nCalls are a key-value pair of methods and their arguments.\n\nIf the execution is not allowed, Airwire will silently fail and simply exclude the call from the request.\n\nIf the execution results in an exception, Airwire will also add `methodName: { exception object }` to the `exceptions` part of the metadata.\n\nExceptions have a complete type definition in TypeScript.\n\n### Validation\n\nValidation is executed on the combination of the current state and the new changes.\n\nProperties that failed validation will have an array of error strings in the `errors` object of the metadata.\n\n## Compared to other solutions\n\nDue to simply being a REST API layer between JavaScript code and a PHP file, Airwire doesn't have to be used *instead* of other libraries. You can use it with anything else.\n\nStill, let's compare it with other libraries to understand when each solution works the best.\n\n### Livewire\n\nLivewire is specifically for returning HTML responses generated using Blade.\n\nMost of our API is inspired by Livewire, with a few minor improvements (such as the use of PHP attributes) that were found *as a result of using Livewire*.\n\nThe best way to think about Livewire and Airwire is that Livewire supports Blade (purely server-rendered), whereas Airwire supports JavaScript (purely frontend-rendered).\n\nNeither one has the ability to support the other approach, so the main deciding factor is what you're using for templating.\n\n(This comparison is putting aside all of the ecosystem differences; it only looks at the tech.)\n\n### Inertia.js\n\nInertia is best thought of as an alternative router for Vue/React/etc. There is some similarity in how Airwire and Inertia are used for a couple of use cases, but for the most part they're very different, since Inertia depends on *visits*, whereas Airwire has no concept of visits or routing.\n\nInertia and Airwire pair well for specific UI components — say that you use Inertia for most things on your frontend, but then you want to build a really dynamic component that sends a lot of requests to the backend (e.g. due to real-time input validation). You could simply install Airwire and use it for that one component, while using Inertia for everything else.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchtechx%2Fairwire","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farchtechx%2Fairwire","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchtechx%2Fairwire/lists"}