Repository: rafaelwendel/phpsupabase
Branch: main
Commit: 835a9f14c825
Files: 9
Total size: 49.6 KB
Directory structure:
gitextract_f93q_x7c/
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
└── src/
├── Auth.php
├── Database.php
├── QueryBuilder.php
└── Service.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/vendor
index.php
tabletestes.php
querybuildertests.php
composer.lock
================================================
FILE: CHANGELOG.md
================================================
# Changes in PHPSupabase #
## 0.0.11 - 2025-10-28
- Add error handling for PostgREST responses in Database and QueryBuilder
- Credit to @Snowbaha for contribution
## 0.0.10 - 2025-06-29
- Fix Prevent error if the uriBase (Service class) is not a valid URL (instead to have : "Warning: Undefined array key "scheme")
- Credit to @Snowbaha for contribution
## 0.0.9 - 2025-05-28
- Add port to URI Base on `Service` to allow connect to local Supabase
- Credit to @jpoto-dev for contribution
- Fix deprecation warnings in PHP 8.4
- Credit to @carlobeltrame for contribution
## 0.0.8 - 2024-08-09
### Fixed
- Fix param tags on `Auth`, `Database`, `QueryBuilder` and `Service` classes
- Credit to @Bartel-C8 for contribution
- Fix `getError` return type on `Auth`, `Database`, `QueryBuilder` and `Service` classes
- Credit to @kevineduardo for contribution
## 0.0.7 - 2023-07-25
### Added
- Add `getHeader` method on Service class
### Changed
- Change the `executeDml` method on Database class to verify if `Prefer` header is defined (Or define the default value).
- Suggested by @nkt-dk
- Change the `where` method on QueryBuilder class (Use `urlencode` function on `$value` variable)
- Credit to @fred-derf for contribution
## 0.0.6 - 2023-06-15
### Added
- Create `limit` method on QueryBuilder class
- Credit to @streeboga for contribution
- Create `limit` option on `createCustomQuery` method (Database class)
- Credit to @streeboga for contribution
## 0.0.5 - 2023-05-17
### Changed
- Change the Service class constructor to accept the URI base without suffix. It is now possible to use a single service instance to create an auth object or database/querybuilder objects.
- Change the `getUriBase` method.
- Change on Auth, Database and QueryBuilder the methods that called `getUriBase`, setting now the suffix (`auth/v1` or `rest/v1`)
### Added
- Create `suffix` attribute in Auth, Database and QueryBuilder classes to set the respective suffix of URI (`auth/v1` or `rest/v1`)
## 0.0.4 - 2022-12-15
### Fixed
- fix: returns an array instead of an object in Database class
## 0.0.3 - 2022-10-10
### Added
- Create `getService` method in Database and QueryBuilder classes
- Create `response` attribute (with `getResponse` method) in Service class to set the Response of requests
## 0.0.2 - 2022-07-22
### Added
- Create `order` method in QueryBuilder class
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) Rafael Wendel Pinheiro <rafaelwendel@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# PHPSupabase
PHPSupabase is a library written in php language, which allows you to use the resources of a project created in Supabase ([supabase.io](https://supabase.io)), through integration with its Rest API.
## Content
- [About Supabase](#about-supabase)
- [PHPSupabase Features](#phpsupabase-features)
- [Instalation & Loading](#instalation-&-features)
- [How to use](#how-to-use)
- [Auth Class](#auth-class)
- [Create a new user with email and password](#create-a-new-user-with-email-and-password)
- [Sign in with email and password](#sign-in-with-email-and-password)
- [Get the data of the logged in user](#get-the-data-of-the-logged-in-user)
- [Update user data](#update-user-data)
- [Database class](#database-class)
- [Insert data](#insert-data)
- [Update data](#update-data)
- [Delete data](#delete-data)
- [Fetch data](#fetch-data)
- [Comparison operators](#comparison-operators)
- [QueryBuilder class](#querybuilder-class)
## About Supabase
Supabase is "The Open Source Firebase Alternative". Through it, is possible to create a backend in less than 2 minutes. Start your project with a Postgres Database, Authentication, instant APIs, realtime subscriptions and Storage.
## PHPSupabase Features
- Create and manage users of a Supabase project
- Manage user authentication (with email/password, magic links, among others)
- Insert, Update, Delete and Fetch data in Postgres Database (by Supabase project Rest API)
- A QueryBuilder class to filter project data in uncomplicated way
## Instalation & loading
PHPSupabase is available on [Packagist](https://packagist.org/packages/rafaelwendel/phpsupabase), and instalation via [Composer](https://getcomposer.org) is the recommended way to install it. Add the follow line to your `composer.json` file:
```json
"rafaelwendel/phpsupabase" : "^0.0.1"
```
or run
```sh
composer require rafaelwendel/phpsupabase
```
## How to use
To use the PHPSupabse library you must have an account and a project created in the Supabase panel. In the project settings (API section), you should note down your project's `API key` and `URL`. (NOTE: Basically we have 2 suffixes to use with the url: `/rest/v1` & `/auth/v1`, but since version `0.0.5` the definition of one of these suffixes is optional)
To start, let's instantiate the `Service()` class. We must pass the `API key` and `url` in the constructor
```php
<?php
require "vendor/autoload.php";
$service = new PHPSupabase\Service(
"YOUR_API_KEY",
"https://aaabbbccc.supabase.co"
);
//In versions 0.0.4 or earlier it is necessary to set the suffix
$service = new PHPSupabase\Service(
"YOUR_API_KEY",
"https://aaabbbccc.supabase.co/auth/v1" // or https://aaabbbccc.supabase.co/rest/v1
);
```
The `Service` class abstracts the actions with the project's API and also provides the instances of the other classes (`Auth`, `Database` and `QueryBuilder`).
### Auth class
Let's instantiate an object of the `Auth` class
```php
$auth = $service->createAuth();
```
The `$auth` object has several methods for managing project users. Through it, it is possible, for example, to create new users or even validate the sign in of an existing user.
#### Create a new user with email and password
See how to create a new user with `email` and `password`.
```php
$auth = $service->createAuth();
try{
$auth->createUserWithEmailAndPassword('newuser@email.com', 'NewUserPassword');
$data = $auth->data(); // get the returned data generated by request
echo 'User has been created! A confirmation link has been sent to the '. $data->email;
}
catch(Exception $e){
echo $auth->getError();
}
```
This newly created user is now in the project's user table and can be seen in the "Authentication" section of the Supabase panel. To be enabled, the user must access the confirmation link sent to the email.
In the third parameter of the `createUserWithEmailAndPassword` method you can pass an array containing the `user_metadata` to be saved (Ex: `name` and `age`)
```php
$user_metadata = [
'name' => 'Lebron James',
'age' => '34'
];
$auth->createUserWithEmailAndPassword('lebron@email.com', 'LebronPassword', $user_metadata);
```
#### Sign in with email and password
Now let's see how to authenticate a user. The Authentication request returns a `access_token` (Bearer Token) that can be used later for other actions and also checks expiration time. In addition, other information such as `refresh_token` and user data are also returned. Invalid login credentials result in throwing a new exception
```php
$auth = $service->createAuth();
try{
$auth->signInWithEmailAndPassword('user@email.com', 'UserPassword');
$data = $auth->data(); // get the returned data generated by request
if(isset($data->access_token)){
$userData = $data->user; //get the user data
echo 'Login successfully for user ' . $userData->email;
//save the $data->access_token in Session, Cookie or other for future requests.
}
}
catch(Exception $e){
echo $auth->getError();
}
```
#### Get the data of the logged in user
To get the user data, you need to have the `access_token` (Bearer Token), which was returned in the login action.
```php
$auth = $service->createAuth();
$bearerToken = 'THE_ACCESS_TOKEN';
try{
$data = $auth->getUser($bearerToken);
print_r($data); // show all user data returned
}
catch(Exception $e){
echo $auth->getError();
}
```
#### Update user data
It is possible to update user data (such as email and password) and also create/update `metadata`, which are additional data that we can create (such as `first_name`, `last_name`, `instagram_account` or any other).
The `updateUser` method must take the `bearerToken` as argument. In addition to it, we have three more optional parameters, which are: `email`, `password` and `data` (array). If you don't want to change some of this data, just set it to `null`.
An example of how to save/update two new meta data (`first_name` and `last_name`) for the user.
```php
$auth = $service->createAuth();
$bearerToken = 'THE_ACCESS_TOKEN';
$newUserMetaData = [
'first_name' => 'Michael',
'last_name' => 'Jordan'
];
try{
//the parameters 2 (email) and 3(password) are null because this data will not be changed
$data = $auth->updateUser($bearerToken, null, null, $newUserMetaData);
print_r($data); // show all user data returned
}
catch(Exception $e){
echo $auth->getError();
}
```
Note that in the array returned now, the keys `first_name` and `last_name` were added to `user_metadata`.
### Database class
The Database class provides features to perform actions (insert, update, delete and fetch) on the Postgre database tables provided by the Supabase project.
For the samples below, consider the following database structure:
```sql
categories (id INT AUTO_INCREMENT, categoryname VARCHAR(32))
products (id INT AUTO_INCREMENT, productname VARCHAR(32), price FLOAT, categoryid INT)
```
The Database class is also instantiated from the `service` object. You must pass the `table` that will be used and its respective primary key (usually `id`).
Let's create an object to work with the `categories` table:
```php
$db = $service->initializeDatabase('categories', 'id');
```
Through the `db` variable it is possible to perform the actions on the `categories` table.
NOTE: If Row Level Security (RLS) is enabled in the used table, pass the `bearerToken` to the `Service` object:
```php
$bearerToken = 'THE_ACCESS_TOKEN'; //returned in the login action.
$db = $service->setBearerToken($bearerToken)->initializeDatabase('categories', 'id');
```
#### Insert data
Inserting a new record in the `categories` table:
```php
$db = $service->initializeDatabase('categories', 'id');
$newCategory = [
'categoryname' => 'Video Games'
];
try{
$data = $db->insert($newCategory);
print_r($data); //returns an array with the new register data
/*
Array
(
[0] => stdClass Object
(
[id] => 1
[categoryname] => Video Games
)
)
*/
}
catch(Exception $e){
echo $e->getMessage();
}
```
Now let's insert a new product from category `1 - Video Games`:
```php
$db = $service->initializeDatabase('products', 'id');
$newProduct = [
'productname' => 'XBOX Series S',
'price' => '299.99',
'categoryid' => '1' //Category "Video Games"
];
try{
$data = $db->insert($newProduct);
print_r($data); //returns an array with the new register data
/*
Array
(
[0] => stdClass Object
(
[id] => 1
[productname] => XBOX Series S
[price] => 299.99
[categoryid] => 1
)
)
*/
}
catch(Exception $e){
echo $e->getMessage();
}
```
#### Update data
To update a record in the database, we use the `update` method, passing as parameter the `id` (PK) of the record to be updated and an `array` containing the new data (NOTE: For now, it is not possible to perform an update using a parameter other than the primary key).
In the example below, we will update the `productname` and `price` of the product with `id=1` ("Xbox Series S" to "XBOX Series S 512GB" and "299.99" to "319.99"):
```php
$db = $service->initializeDatabase('products', 'id');
$updateProduct = [
'productname' => 'XBOX Series S 512GB',
'price' => '319.99'
];
try{
$data = $db->update('1', $updateProduct); //the first parameter ('1') is the product id
print_r($data); //returns an array with the product data (updated)
/*
Array
(
[0] => stdClass Object
(
[id] => 1
[productname] => XBOX Series S 512GB
[price] => 319.99
[categoryid] => 1
)
)
*/
}
catch(Exception $e){
echo $e->getMessage();
}
```
#### Delete data
To delete a record from the table, just call the `delete` method and pass the `id` (PK) of the record to be deleted as a parameter.
The following code deletes the product of `id=1` in the `products` table:
```php
$db = $service->initializeDatabase('products', 'id');
try{
$data = $db->delete('1'); //the parameter ('1') is the product id
echo 'Product deleted successfully';
}
catch(Exception $e){
echo $e->getMessage();
}
```
#### Fetch data
The following methods for fetching data are available in the `Database` class:
- `fetchAll()`: fetch all table records;
- `findBy(string $column, string $value)`: fetch records filtereds by a column/value (using the `=` operator);
- `findByLike(string $column, string $value)`: fetch records filtereds by a column/value (using the `LIKE` operator);
- `join(string $foreignTable, string $foreignKey)`: make a `join` between the seted table and another table related and fetch records;
- `createCustomQuery(array $args)`: build a custom SQL query. The following `keys` are valid for the `args` argument:
- `select`
- `from`
- `join`
- `where`
- `limit`
- `range`
All the mentioned methods return the self instance of `Database` class. To access the fetched data, call the `getResult` method.
See some examples:
```php
$db = $service->initializeDatabase('products', 'id');
try{
$listProducts = $db->fetchAll()->getResult(); //fetch all products
foreach ($listProducts as $product){
echo $product->id . ' - ' . $product->productname . '($' . $product->price . ') <br />';
}
}
catch(Exception $e){
echo $e->getMessage();
}
```
Now, an example using the `findBy` method:
```php
$db = $service->initializeDatabase('products', 'id');
try{
$listProducts = $db->findBy('productname', 'PlayStation 5')->getResult(); //Searches for products that have the value "PlayStation 5" in the "productname" column
foreach ($listProducts as $product){
echo $product->id . ' - ' . $product->productname . '($' . $product->price . ') <br />';
}
}
catch(Exception $e){
echo $e->getMessage();
}
```
Searching for `products` and adding a join with the `categories` table:
```php
$db = $service->initializeDatabase('products', 'id');
try{
$listProducts = $db->join('categories', 'id')->getResult(); //fetch data from "products" JOIN "categories"
foreach ($listProducts as $product){
//SHOW "productname" - "categoryname"
echo $product->productname . ' - ' . $product->categories->categoryname . '<br />';
}
}
catch(Exception $e){
echo $e->getMessage();
}
```
An example of a custom query to search `id,productname,price` for all `products` "JOIN" `categories` filtering by `price` (`price` greater than `200.00`):
```php
$db = $service->initializeDatabase('products', 'id');
$query = [
'select' => 'id,productname,price',
'from' => 'products',
'join' => [
[
'table' => 'categories',
'tablekey' => 'id'
]
],
'where' =>
[
'price' => 'gt.200' //"gt" means "greater than" (>)
]
];
try{
$listProducts = $db->createCustomQuery($query)->getResult();
foreach ($listProducts as $product){
echo $product->id . ' - ' . $product->productname . '($' . $product->price . ') <br />';
}
}
catch(Exception $e){
echo $e->getMessage();
}
```
Other examples for custom query:
```php
//products with price > 200 AND productname LIKE '%n%'
$query = [
'select' => 'id,productname,price',
'from' => 'products',
'where' =>
[
'price' => 'gt.200', //"gt" means "greater than" (>)
'productname' => 'like.%n%' //like operator
]
];
//products with categoryid = 1
$query = [
'select' => 'id,productname,price',
'from' => 'products',
'where' =>
[
'categoryid' => 'eq.1', //"eq" means "equal" (=)
]
];
//products with price < 1000 LIMIT 4 results
$query = [
'select' => 'id,productname,price',
'from' => 'products',
'where' =>
[
'price' => 'lt.1000', //"lt" means "less than" (<)
],
'limit' => 4 //4 first rows
];
```
#### Comparison operators
The main operators available for the `where` clause:
- `eq`: equal
- `neq`: not equal
- `gt`: greater than
- `gte`: greater than or equal
- `lt`: less than
- `lte`: less than or equal
- `like`: search for a specified pattern in a column
- `ilike`: search for a specified pattern in a column (case insensitive)
Other operators available:
- `is`
- `in`
- `cs`
- `cd`
- `sl`
- `sr`
- `nxl`
- `nxr`
- `adj`
- `ov`
- `fts`
- `plfts`
- `phfts`
- `wfts`
- `not.eq`
- `not.neq`
- `not.gt`
- `not.gte`
- `not.lt`
- `not.lte`
- `not.like`
- `not.ilike`
- `not.is`
- `not.in`
- `not.cs`
- `not.cd`
- `not.sl`
- `not.sr`
- `not.nxl`
- `not.nxr`
- `not.adj`
- `not.ov`
- `not.fts`
- `not.plfts`
- `not.phfts`
- `not.wfts`
### QueryBuilder class
The QueryBuilder class provides methods for dynamically building SQL queries. It is instantiated from the `service` object.
```php
$query = $service->initializeQueryBuilder();
```
NOTE: If Row Level Security (RLS) is enabled on any of the tables used, pass the `bearerToken` to the `Service` object:
```php
$bearerToken = 'THE_ACCESS_TOKEN'; //returned in the login action.
$query = $service->setBearerToken($bearerToken)->initializeQueryBuilder();
```
Available methods:
- `select(string $select)`: the fields (comma separated) or `*`
- `from(string $from)`: the table
- `join(string $table, string $tablekey, string $select = null)`: related table
- `where(string $column, string $value)`: conditions
- `limit(int $limit)`: limit rows
- `order(string $order)`: the "order by" field
- `range(string $range)`: results range (E.g. "0-3")
All the mentioned methods return the self instance of `QueryBuilder` class. To run the mounted query, call the `execute` method. Then, to access the fetched data, call the `getResult` method.
An example to fetch all data from the `products` table:
```php
$query = $service->initializeQueryBuilder();
try{
$listProducts = $query->select('*')
->from('products')
->execute()
->getResult();
foreach ($listProducts as $product){
echo $product->id . ' - ' . $product->productname . '($' . $product->price . ') <br />';
}
}
catch(Exception $e){
echo $e->getMessage();
}
```
An example to fetch all data from the `products` "JOIN" `categories`:
```php
$query = $service->initializeQueryBuilder();
try{
$listProducts = $query->select('*')
->from('products')
->join('categories', 'id')
->execute()
->getResult();
foreach ($listProducts as $product){
echo $product->id . ' - ' . $product->productname . '($' . $product->price . ') - '. $product->categories->categoryname .' <br />';
}
}
catch(Exception $e){
echo $e->getMessage();
}
```
Fetch `products` with `categoryid=1` and `price>200` order by `price`:
```php
$query = $service->initializeQueryBuilder();
try{
$listProducts = $query->select('*')
->from('products')
->join('categories', 'id')
->where('categoryid', 'eq.1') //eq -> equal
->where('price', 'gt.200') // gt -> greater than
->order('price.asc') //"price.desc" for descending
->execute()
->getResult();
foreach ($listProducts as $product){
echo $product->id . ' - ' . $product->productname . '($' . $product->price . ') - '. $product->categories->categoryname .' <br />';
}
}
catch(Exception $e){
echo $e->getMessage();
}
```
Some of the operators to be used in the `where` method can be seen in the [Comparison operators](#comparison-operators) section.
================================================
FILE: composer.json
================================================
{
"name": "rafaelwendel/phpsupabase",
"description" : "PHPSupabase is a library written in php language, which allows you to use the resources of a project created in Supabase (https://supabase.io), through integration with its Rest API.",
"keywords": ["supabase", "php", "client"],
"version" : "0.0.11",
"type": "library",
"homepage": "https://github.com/rafaelwendel/phpsupabase",
"license": "MIT",
"authors": [
{
"name": "Rafael Wendel Pinheiro",
"email": "rafaelwendel@hotmail.com"
}
],
"minimum-stability": "stable",
"require": {
"guzzlehttp/guzzle": "^7.0"
},
"autoload": {
"psr-4": {
"PHPSupabase\\": "src/"
}
}
}
================================================
FILE: src/Auth.php
================================================
<?php
namespace PHPSupabase;
class Auth {
private $suffix = 'auth/v1/';
private $service;
private $data;
/**
* Construct method (Set the Service instance)
* @access public
* @param Service $service The Supabase Service instance
* @return void
*/
public function __construct(Service $service)
{
$this->service = $service;
}
/**
* Returns the response data produced by a requisition
* @access public
* @return object
*/
public function data() : object
{
return $this->data;
}
/**
* Returns the error generated
* @access public
* @return string|null
*/
public function getError() : string|null
{
return $this->service->getError();
}
/**
* Default method to call POST requests to users management
* @access private
* @param string $endPoint The endpoint of request
* @param array $fields The body fields to be use in request (Ex: email, password, ...)
* @return void
*/
private function defaultPostCallUserManagement(string $endPoint, array $fields) : void
{
$uri = $this->service->getUriBase($this->suffix . $endPoint);
$options = [
'headers' => $this->service->getHeaders(),
'body' => json_encode($fields)
];
$this->data = $this->service->executeHttpRequest('POST', $uri, $options);
}
/**
* Create a new user (by email and password) in Supabase project
* @access public
* @param string $email The email address of new user
* @param string $password The password of new user
* @param array $data Optional. The user meta data
* @return void
*/
public function createUserWithEmailAndPassword(string $email, string $password, array $data = []) : void
{
$fields = [
'email' => $email,
'password' => $password
];
if(is_array($data) && count($data) > 0){
$fields['data'] = $data;
}
$this->defaultPostCallUserManagement('signup', $fields);
}
/**
* Sign in (authenticate) in Supabase project (by email and password)
* @access public
* @param string $email The user email
* @param string $password The user password
* @return void
*/
public function signInWithEmailAndPassword(string $email, string $password) : void
{
$fields = [
'email' => $email,
'password' => $password
];
$this->defaultPostCallUserManagement('token?grant_type=password', $fields);
}
/**
* Sign in (authenticate) in Supabase project (by refresh token)
* @access public
* @param string $refreshToken The refresh token
* @return void
*/
public function signInWithRefreshToken(string $refreshToken) : void
{
$fields = [
'refresh_token' => $refreshToken
];
$this->defaultPostCallUserManagement('token?grant_type=refresh_token', $fields);
}
/**
* Sign in (authenticate) in Supabase project (by magic link sended to user email)
* @access public
* @param string $email The user email
* @return void
*/
public function signInWithMagicLink(string $email) : void
{
$fields = [
'email' => $email
];
$this->defaultPostCallUserManagement('magiclink', $fields);
}
/**
* Create a new user (by phone and password) in Supabase project
* @access public
* @param string $phone The phone number of new user
* @param string $password The password of new user
* @param array $data Optional. The user meta data
* @return void
*/
public function createUserWithPhoneAndPassword(string $phone, string $password, array $data = []) : void
{
$fields = [
'phone' => $phone,
'password' => $password
];
if(is_array($data) && count($data) > 0){
$fields['data'] = $data;
}
$this->defaultPostCallUserManagement('signup', $fields);
}
/**
* Sign in (authenticate) in Supabase project (by SMS OTP)
* @access public
* @param string $phone The user phone number
* @return void
*/
public function signInWithSMSOTP(string $phone) : void
{
$fields = [
'phone' => $phone
];
$this->defaultPostCallUserManagement('otp', $fields);
}
/**
* Recover the user password (by a link sended to user email)
* @access public
* @param string $email The user email
* @return void
*/
public function recoverPassword(string $email) : void
{
$fields = [
'email' => $email
];
$this->defaultPostCallUserManagement('recover', $fields);
}
/**
* Logout
* @access public
* @param string $bearerUserToken The bearer user token (generated in sign in process)
* @return array|object|null
*/
public function logout(string $bearerUserToken)
{
$uri = $this->service->getUriBase($this->suffix . 'logout');
$this->service->setHeader('Authorization', 'Bearer ' . $bearerUserToken);
$options = [
'headers' => $this->service->getHeaders()
];
return $this->service->executeHttpRequest('POST', $uri, $options);
}
/**
* Returns the user data
* @access public
* @param string $bearerUserToken The bearer user token (generated in sign in process)
* @return array|object|null
*/
public function getUser(string $bearerUserToken)
{
$uri = $this->service->getUriBase($this->suffix . 'user');
$this->service->setHeader('Authorization', 'Bearer ' . $bearerUserToken);
$options = [
'headers' => $this->service->getHeaders()
];
return $this->service->executeHttpRequest('GET', $uri, $options);
}
/**
* Verify if the user is authenticated
* @access public
* @param string $bearerUserToken The bearer user token (generated in sign in process)
* @return bool
*/
public function isAuthenticated(string $bearerUserToken) : bool
{
$data = $this->getUser($bearerUserToken);
return $data->aud == 'authenticated'
? true
: false;
}
/**
* Update the user data
* @access public
* @param string $bearerUserToken The bearer user token (generated in sign in process)
* @param string $email Optional. The user email
* @param string $password Optional. The user password
* @param array $data Optional. The user meta data
* @return array|object|null
*/
public function updateUser(string $bearerUserToken, ?string $email = null, ?string $password = null, array $data = [])
{
$uri = $this->service->getUriBase($this->suffix . 'user');
$this->service->setHeader('Authorization', 'Bearer ' . $bearerUserToken);
$fields = [];
if(!is_null($email)){
$fields['email'] = $email;
}
if(!is_null($password)){
$fields['password'] = $password;
}
if(is_array($data) && count($data) > 0){
$fields['data'] = $data;
}
$options = [
'headers' => $this->service->getHeaders(),
'body' => json_encode($fields)
];
return $this->service->executeHttpRequest('PUT', $uri, $options);
}
}
================================================
FILE: src/Database.php
================================================
<?php
namespace PHPSupabase;
use Exception;
class Database {
private $suffix = 'rest/v1/';
private $service;
private $tableName;
private $primaryKey;
private $result;
/**
* Construct method (Set the Service instance, the table to be used and the table primary key)
* @access public
* @param Service $service The Supabase Service instance
* @param string $tableName The table
* @param string $primaryKey The table primary key
* @return void
*/
public function __construct(Service $service, string $tableName, string $primaryKey)
{
$this->service = $service;
$this->tableName = $tableName;
$this->primaryKey = $primaryKey;
}
/**
* Returns the Service instance
* @access public
* @return Service
*/
public function getService() : Service
{
return $this->service;
}
/**
* Returns the error generated
* @access public
* @return string|null
*/
public function getError() : string|null
{
return $this->service->getError();
}
/**
* Returns the result (data) generated by a fetch
* @access public
* @return array
*/
public function getResult() : array
{
if ($this->service->isPostgrestError($this->result)) {
return throw new Exception($this->result->message);
}
return $this->result;
}
/**
* Returns the first result (data) generated by a fetch
* @access public
* @return object
*/
public function getFirstResult() : object
{
return count($this->result) > 0
? $this->result[0]
: new \stdClass;
}
/**
* Execute a query in database
* @access private
* @param string $queryString The parameters to be used in the request
* @param string $table String Optional. Use a different table that the set in construct method
* @return void
*/
private function executeQuery(string $queryString, ?string $table = null) : void
{
$table = is_null($table)
? $this->tableName
: $table;
$uri = $this->service->getUriBase($this->suffix . $table . '?' . $queryString);
$options = [
'headers' => $this->service->getHeaders()
];
$this->result = $this->service->executeHttpRequest('GET', $uri, $options);
}
/**
* Execute a DML (Data Manipulation Language) query in database
* @access private
* @param string $method The request method (GET, POST, PUT, DELETE, PATCH, ...)
* @param array $data The fields to be used in query/request
* @param string $queryString Optional. The parameters to be used in the requests
* @return array|object|null
*/
private function executeDml(string $method, array $data, ?string $queryString = null)
{
$endPoint = ($queryString == null) ? $this->tableName : $this->tableName . '?' . $queryString;
$uri = $this->service->getUriBase($this->suffix . $endPoint);
if(is_null($this->service->getHeader('Prefer'))) {
$this->service->setHeader('Prefer', 'return=representation');
}
$options = [
'headers' => $this->service->getHeaders(),
'body' => json_encode($data)
];
return $this->service->executeHttpRequest($method, $uri, $options);
}
/**
* Insert a new register into table
* @access public
* @param array $data The values to be inserted
* @return array|object|null
*/
public function insert(array $data)
{
return $this->executeDml('POST', $data);
}
/**
* Update a register into table
* @access public
* @param string $id The "id" (PK) of the register, to be used in WHERE clause
* @param array $data The values to be updated
* @return array|object|null
*/
public function update(string $id, array $data)
{
return $this->executeDml('PATCH', $data, $this->primaryKey . '=eq.' . $id);
}
/**
* Delete a register into table
* @access public
* @param string $id The "id" (PK) of the register, to be used in WHERE clause
* @return array|object|null
*/
public function delete(string $id)
{
return $this->executeDml('DELETE', [], $this->primaryKey . '=eq.' . $id);
}
/**
* Fetch all registers of table
* @access public
* @return Database
*/
public function fetchAll() : Database
{
$this->executeQuery('select=*');
return $this;
}
/**
* Fetch registers of table by a especific column/value
* @access public
* @param string $column The column name
* @param string $value The value
* @return Database
*/
public function findBy(string $column, string $value) : Database
{
$this->executeQuery($column . '=eq.' . $value);
return $this;
}
/**
* Fetch registers of table by a especific column/value, using LIKE operator
* @access public
* @param string $column The column name
* @param string $value The value
* @return Database
*/
public function findByLike(string $column, string $value) : Database
{
$this->executeQuery($column . '=like.%' . $value . '%');
return $this;
}
/**
* Make a "join" between the seted table and another table related
* @access public
* @param string $foreignTable The related table
* @param string $foreignKey The foreign key (usually "id")
* @return Database
*/
public function join(string $foreignTable, string $foreignKey) : Database
{
$this->executeQuery('select=*,' . $foreignTable . '(' . $foreignKey . ', *)');
return $this;
}
/**
* Create a custom query to fetch into database
* @access public
* @param array $args The query structure (Available keys: "select", "from", "join", "where", "range")
* @return Database
*/
public function createCustomQuery(array $args) : Database
{
$queryBuilder = $this->service->initializeQueryBuilder();
$select = isset($args['select'])
? $args['select']
: '*';
$queryBuilder->select($select);
$from = isset($args['from'])
? $args['from']
: $this->tableName;
$queryBuilder->from($from);
if(isset($args['join'])){
if(is_array($args['join']) && count($args['join']) > 0){
foreach ($args['join'] as $join){
if(is_array($join) && isset($join['table']) && isset($join['tablekey'])){
$select = isset($join['select'])
? $join['select']
: null;
$queryBuilder->join($join['table'], $join['tablekey'], $select);
}
else{
throw new Exception('"JOIN" argument must have "table" and "tablekey" keys');
}
}
}
else {
throw new Exception('"JOIN" argument must be an array');
}
}
if(isset($args['where'])){
if(is_array($args['where']) && count($args['where']) > 0){
foreach ($args['where'] as $key => $where){
$queryBuilder->where($key, $where);
}
}
else{
throw new Exception('"WHERE" argument must be an array');
}
}
if(isset($args['limit'])){
$queryBuilder->limit($args['limit']);
}
if(isset($args['order'])){
$queryBuilder->order($args['order']);
}
if(isset($args['range'])){
$queryBuilder->range($args['range']);
}
$this->result = $queryBuilder->execute()->getResult();
return $this;
}
}
================================================
FILE: src/QueryBuilder.php
================================================
<?php
namespace PHPSupabase;
use Exception;
use GuzzleHttp\Psr7\Query;
class QueryBuilder {
private $suffix = 'rest/v1/';
private $service;
private $query;
private $result;
/**
* Construct method (Set the Service instance)
* @access public
* @param Service $service The Supabase Service instance
* @return void
*/
public function __construct(Service $service)
{
$this->service = $service;
}
/**
* Returns the Service instance
* @access public
* @return Service
*/
public function getService() : Service
{
return $this->service;
}
/**
* Returns the result (data) generated by a fetch
* @access public
* @return array
*/
public function getResult() : array
{
if ($this->service->isPostgrestError($this->result)) {
return throw new Exception($this->result->message);
}
return $this->result;
}
/**
* Returns the first result (data) generated by a fetch
* @access public
* @return object
*/
public function getFirstResult() : mixed
{
return count($this->result) > 0
? $this->result[0]
: [];
}
/**
* Returns the error generated
* @access public
* @return string|null
*/
public function getError() : string|null
{
return $this->service->getError();
}
/**
* Add the "select" to the query
* @access public
* @param string $select The select (Ex: * OR column1, column2, ...)
* @return QueryBuilder
*/
public function select(string $select) : QueryBuilder
{
$this->query['select'] = $select;
return $this;
}
/**
* Add the "from" to the query
* @access public
* @param string $from The table to be used in query
* @return QueryBuilder
*/
public function from(string $from) : QueryBuilder
{
$this->query['from'] = $from;
return $this;
}
/**
* Make a "join" between the "from" table and another table related
* @access public
* @param string $tableable The related table
* @param string $tablekey The foreign key (usually "id")
* @param string $select Optional. The columns to be select in foreign table
* @return QueryBuilder
*/
public function join(string $table, string $tablekey, ?string $select = null) : QueryBuilder
{
$this->query['join'][] = $table .
'(' . $tablekey . ',' .
(!is_null($select) ? $select : '*') . ')';
return $this;
}
/**
* Add the condition "where" to the query
* @access public
* @param string $column The column to be used in where clause
* @param string $value The value of condition
* @return QueryBuilder
*/
public function where(string $column, string $value) : QueryBuilder
{
$this->query['where'][] = $column . '=' . urlencode($value);
return $this;
}
/**
* Add the "limit" to the query
* @access public
* @param int $limit The interval of fetch registers
* @return QueryBuilder
*/
public function limit(int $limit) : QueryBuilder
{
$this->query['limit'] = $limit;
return $this;
}
/**
* Add the "order" to the query
* @access public
* @param string $order The order by column
* @return QueryBuilder
*/
public function order(string $order) : QueryBuilder
{
$this->query['order'] = $order;
return $this;
}
/**
* Add the "range" to the query
* @access public
* @param string $range The interval of fetch registers
* @return QueryBuilder
*/
public function range(string $range) : QueryBuilder
{
$this->query['range'] = $range;
return $this;
}
/**
* Execute (prepare) the mounted query
* @access public
* @return QueryBuilder
*/
public function execute() : QueryBuilder
{
$this->query['select'] = isset($this->query['select'])
? $this->query['select']
: '*';
if(!isset($this->query['from'])){
throw new Exception('The table is not defined');
}
$queryString = 'select=' . $this->query['select'];
if(isset($this->query['join'])){
$queryString .= ',' . implode(',', $this->query['join']);
}
if(isset($this->query['where'])){
$queryString .= '&' . implode('&', $this->query['where']);
}
if(isset($this->query['limit'])){
$queryString .= '&limit=' . $this->query['limit'];
}
if(isset($this->query['order'])){
$queryString .= '&order=' . $this->query['order'];
}
if(isset($this->query['range'])){
$this->service->setHeader('Range', $this->query['range']);
}
$this->executeQuery($queryString);
return $this;
}
/**
* Execute the request to run the query
* @access private
* @param string $queryString A query string to be use in URL
* @return void
*/
private function executeQuery(string $queryString) : void
{
$uri = $this->service->getUriBase($this->suffix . $this->query['from'] . '?' . $queryString);
$options = [
'headers' => $this->service->getHeaders()
];
$this->result = $this->service->executeHttpRequest('GET', $uri, $options);
}
}
================================================
FILE: src/Service.php
================================================
<?php
namespace PHPSupabase;
use GuzzleHttp\Psr7\Response;
class Service
{
private $apiKey;
private $uriBase;
private $httpClient;
private $error;
private $response;
private $headers = [
'Content-Type' => 'application/json'
];
/**
* Construct method (Set the API key, URI base and instance GuzzleHttp client)
* @access public
* @param string $apiKey The Supabase project API Key
* @param string $uriBase API URI base (Ex: "https://abcdefgh.supabase.co/rest/v1/" OR "https://abcdefgh.supabase.co/auth/v1/")
* @return void
*/
public function __construct(string $apiKey, string $uriBase)
{
$this->apiKey = $apiKey;
$this->uriBase = $this->formatUriBase($uriBase);
$this->httpClient = new \GuzzleHttp\Client();
$this->headers['apikey'] = $this->apiKey;
}
/**
* Set bearerToken to be added into headers and to be used for future requests
* @access public
* @param string $bearerToken The bearer user token (generated in sign in process)
* @return Service
*/
public function setBearerToken($bearerToken)
{
$this->setHeader('Authorization', 'Bearer ' . $bearerToken);
return $this;
}
/**
* Format URI base with slash at end
* @access private
* @param string $uriBase API URI base (Ex: "https://abcdefgh.supabase.co/rest/v1/" OR "https://abcdefgh.supabase.co/auth/v1/")
* @return string
*/
private function formatUriBase(string $uriBase): string
{
return (substr($uriBase, -1) == '/')
? $uriBase
: $uriBase . '/';
}
/**
* Returns the API key
* @access public
* @return string
*/
public function getApiKey(): string
{
return $this->apiKey;
}
/**
* Returns the URI base
* @access public
* @param string $endPoint Optional. String The end point to concatenate to URI base
* @return string
*/
public function getUriBase(string $endPoint = ''): string
{
$parseUrl = parse_url($this->uriBase);
$parseUrl['port'] = isset($parseUrl['port']) ? $parseUrl['port'] : null;
if ($parseUrl['port'] === null) {
if (!isset($parseUrl['scheme']) || $parseUrl['scheme'] === 'http') {
$parseUrl['port'] = 80;
} elseif ($parseUrl['scheme'] === 'https') {
$parseUrl['port'] = 443;
}
}
// Prevent error if the uriBase is not a valid url
if(!isset($parseUrl['scheme']) || !isset($parseUrl['host'])){
return $this->uriBase . $endPoint;
}
return $parseUrl['scheme'] . '://' . $parseUrl['host'] . ':' . $parseUrl['port'] . '/' . $endPoint;
}
/**
* Returns the HTTP Client (GuzzleHttp)
* @access public
* @return \GuzzleHttp\Client
*/
public function getHttpClient(): \GuzzleHttp\Client
{
return $this->httpClient;
}
/**
* Returns the Response of last request
* @access public
* @return Response
*/
public function getResponse(): Response
{
return $this->response;
}
/**
* Set a header to be use in the API request
* @access public
* @param string $header The header key to be set
* @param string $value The value of header
* @return void
*/
public function setHeader(string $header, string $value): void
{
$this->headers[$header] = $value;
}
/**
* Returns a specific header or null if it doesn't exist
* @access public
* @param string $header The header key to be set
* @return string|null
*/
public function getHeader(string $header)
{
return (isset($this->headers[$header]))
? $this->headers[$header]
: null;
}
/**
* Returns the set headers
* @access public
* @return array
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Returns the error generated
* @access public
* @return string|null
*/
public function getError(): string|null
{
return $this->error;
}
/**
* Check if the last response contains an error
* @access public
* @return bool
*/
public function hasError(): bool
{
return !is_null($this->error);
}
/**
* Check if the response is a PostgREST error
* @access public
* @param mixed $response The response to check
* @return bool
*/
public function isPostgrestError($response): bool
{
return is_object($response) && isset($response->code) && isset($response->message);
}
/**
* Returns a new instance of Auth class
* @access public
* @return Auth
*/
public function createAuth(): Auth
{
return new Auth($this);
}
/**
* Returns a new instance of Database class
* @access public
* @param string $tableName The table to be used
* @param string $primaryKey Optional. String The table primary key (usually "id")
* @return Database
*/
public function initializeDatabase(string $tableName, string $primaryKey = 'id'): Database
{
return new Database($this, $tableName, $primaryKey);
}
/**
* Returns a new instance of QueryBuilder class
* @access public
* @return QueryBuilder
*/
public function initializeQueryBuilder(): QueryBuilder
{
return new QueryBuilder($this);
}
/**
* Format the exception thrown by GuzzleHttp, formatting the error message
* @access public
* @param \GuzzleHttp\Exception\RequestException $e The exception thrown by GuzzleHttp
* @return void
*/
public function formatRequestException(\GuzzleHttp\Exception\RequestException $e): void
{
if ($e->hasResponse()) {
$res = json_decode($e->getResponse()->getBody());
$searchItems = ['msg', 'message', 'error_description'];
foreach ($searchItems as $item) {
if (isset($res->$item)) {
$this->error = $res->$item;
break;
}
}
}
}
/**
* Execute a Http request in Supabase API
* @access public
* @param string $method The request method (GET, POST, PUT, DELETE, PATCH, ...)
* @param string $uri The URI to be requested (including the endpoint)
* @param array $options Requisition options (header, body, ...)
* @return array|object|null
*/
public function executeHttpRequest(string $method, string $uri, array $options)
{
try {
$this->response = $this->httpClient->request(
$method,
$uri,
$options
);
return json_decode($this->response->getBody());
} catch (\GuzzleHttp\Exception\RequestException $e) {
$this->formatRequestException($e);
throw $e;
} catch (\GuzzleHttp\Exception\ConnectException $e) {
$this->error = $e->getMessage();
throw $e;
}
}
}
gitextract_f93q_x7c/
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
└── src/
├── Auth.php
├── Database.php
├── QueryBuilder.php
└── Service.php
SYMBOL INDEX (66 symbols across 4 files)
FILE: src/Auth.php
class Auth (line 5) | class Auth {
method __construct (line 16) | public function __construct(Service $service)
method data (line 26) | public function data() : object
method getError (line 36) | public function getError() : string|null
method defaultPostCallUserManagement (line 48) | private function defaultPostCallUserManagement(string $endPoint, array...
method createUserWithEmailAndPassword (line 66) | public function createUserWithEmailAndPassword(string $email, string $...
method signInWithEmailAndPassword (line 86) | public function signInWithEmailAndPassword(string $email, string $pass...
method signInWithRefreshToken (line 101) | public function signInWithRefreshToken(string $refreshToken) : void
method signInWithMagicLink (line 115) | public function signInWithMagicLink(string $email) : void
method createUserWithPhoneAndPassword (line 131) | public function createUserWithPhoneAndPassword(string $phone, string $...
method signInWithSMSOTP (line 150) | public function signInWithSMSOTP(string $phone) : void
method recoverPassword (line 164) | public function recoverPassword(string $email) : void
method logout (line 178) | public function logout(string $bearerUserToken)
method getUser (line 194) | public function getUser(string $bearerUserToken)
method isAuthenticated (line 210) | public function isAuthenticated(string $bearerUserToken) : bool
method updateUser (line 227) | public function updateUser(string $bearerUserToken, ?string $email = n...
FILE: src/Database.php
class Database (line 7) | class Database {
method __construct (line 22) | public function __construct(Service $service, string $tableName, strin...
method getService (line 34) | public function getService() : Service
method getError (line 44) | public function getError() : string|null
method getResult (line 54) | public function getResult() : array
method getFirstResult (line 67) | public function getFirstResult() : object
method executeQuery (line 81) | private function executeQuery(string $queryString, ?string $table = nu...
method executeDml (line 101) | private function executeDml(string $method, array $data, ?string $quer...
method insert (line 123) | public function insert(array $data)
method update (line 135) | public function update(string $id, array $data)
method delete (line 146) | public function delete(string $id)
method fetchAll (line 156) | public function fetchAll() : Database
method findBy (line 169) | public function findBy(string $column, string $value) : Database
method findByLike (line 182) | public function findByLike(string $column, string $value) : Database
method join (line 195) | public function join(string $foreignTable, string $foreignKey) : Database
method createCustomQuery (line 207) | public function createCustomQuery(array $args) : Database
FILE: src/QueryBuilder.php
class QueryBuilder (line 8) | class QueryBuilder {
method __construct (line 20) | public function __construct(Service $service)
method getService (line 30) | public function getService() : Service
method getResult (line 40) | public function getResult() : array
method getFirstResult (line 53) | public function getFirstResult() : mixed
method getError (line 65) | public function getError() : string|null
method select (line 76) | public function select(string $select) : QueryBuilder
method from (line 88) | public function from(string $from) : QueryBuilder
method join (line 102) | public function join(string $table, string $tablekey, ?string $select ...
method where (line 117) | public function where(string $column, string $value) : QueryBuilder
method limit (line 129) | public function limit(int $limit) : QueryBuilder
method order (line 141) | public function order(string $order) : QueryBuilder
method range (line 153) | public function range(string $range) : QueryBuilder
method execute (line 164) | public function execute() : QueryBuilder
method executeQuery (line 205) | private function executeQuery(string $queryString) : void
FILE: src/Service.php
class Service (line 7) | class Service
method __construct (line 26) | public function __construct(string $apiKey, string $uriBase)
method setBearerToken (line 41) | public function setBearerToken($bearerToken)
method formatUriBase (line 53) | private function formatUriBase(string $uriBase): string
method getApiKey (line 65) | public function getApiKey(): string
method getUriBase (line 76) | public function getUriBase(string $endPoint = ''): string
method getHttpClient (line 101) | public function getHttpClient(): \GuzzleHttp\Client
method getResponse (line 111) | public function getResponse(): Response
method setHeader (line 123) | public function setHeader(string $header, string $value): void
method getHeader (line 134) | public function getHeader(string $header)
method getHeaders (line 146) | public function getHeaders(): array
method getError (line 156) | public function getError(): string|null
method hasError (line 166) | public function hasError(): bool
method isPostgrestError (line 177) | public function isPostgrestError($response): bool
method createAuth (line 187) | public function createAuth(): Auth
method initializeDatabase (line 199) | public function initializeDatabase(string $tableName, string $primaryK...
method initializeQueryBuilder (line 209) | public function initializeQueryBuilder(): QueryBuilder
method formatRequestException (line 220) | public function formatRequestException(\GuzzleHttp\Exception\RequestEx...
method executeHttpRequest (line 243) | public function executeHttpRequest(string $method, string $uri, array ...
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (53K chars).
[
{
"path": ".gitignore",
"chars": 69,
"preview": "/vendor\nindex.php\ntabletestes.php\nquerybuildertests.php\ncomposer.lock"
},
{
"path": "CHANGELOG.md",
"chars": 2426,
"preview": "# Changes in PHPSupabase #\n\n## 0.0.11 - 2025-10-28\n\n- Add error handling for PostgREST responses in Database and QueryBu"
},
{
"path": "LICENSE",
"chars": 1100,
"preview": "MIT License\n\nCopyright (c) Rafael Wendel Pinheiro <rafaelwendel@hotmail.com>\n\nPermission is hereby granted, free of char"
},
{
"path": "README.md",
"chars": 17930,
"preview": "# PHPSupabase\n\nPHPSupabase is a library written in php language, which allows you to use the resources of a project crea"
},
{
"path": "composer.json",
"chars": 753,
"preview": "{\n \"name\": \"rafaelwendel/phpsupabase\",\n \"description\" : \"PHPSupabase is a library written in php language, which a"
},
{
"path": "src/Auth.php",
"chars": 7527,
"preview": "<?php\n\nnamespace PHPSupabase;\n\nclass Auth {\n private $suffix = 'auth/v1/';\n private $service;\n private $data;\n\n"
},
{
"path": "src/Database.php",
"chars": 8107,
"preview": "<?php\n\nnamespace PHPSupabase;\n\nuse Exception;\n\nclass Database {\n private $suffix = 'rest/v1/';\n private $service;\n"
},
{
"path": "src/QueryBuilder.php",
"chars": 5645,
"preview": "<?php\n\nnamespace PHPSupabase;\n\nuse Exception;\nuse GuzzleHttp\\Psr7\\Query;\n\nclass QueryBuilder {\n private $suffix = 're"
},
{
"path": "src/Service.php",
"chars": 7236,
"preview": "<?php\n\nnamespace PHPSupabase;\n\nuse GuzzleHttp\\Psr7\\Response;\n\nclass Service\n{\n private $apiKey;\n private $uriBase;"
}
]
About this extraction
This page contains the full source code of the rafaelwendel/phpsupabase GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (49.6 KB), approximately 13.0k tokens, and a symbol index with 66 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.