tobento/service-storage

A storage interface for storing and fetching items.

1.2.7 2024-08-13 14:14 UTC

README

The Storage Service comes with a query builder for storing and fetching items.

Table of Contents

Getting started

Add the latest version of the Storage service running this command.

composer require tobento/service-storage

Requirements

  • PHP 8.0 or greater

Highlights

  • Framework-agnostic, will work with any project
  • Decoupled design
  • Query Builder
  • PDO MariaDb Storage
  • PDO MySql Storage
  • Json File Storage
  • In Memory Storage

Simple Example

Here is a simple example of how to use the Storage service.

use Tobento\Service\Storage\Tables\Tables;
use Tobento\Service\Storage\JsonFileStorage;
use Tobento\Service\Storage\ItemInterface;

$tables = new Tables();
$tables->add('products', ['id', 'sku', 'price'], 'id');

$storage = new JsonFileStorage(
    dir: 'home/private/storage/',
    tables: $tables
);

$inserted = $storage
    ->table('products')
    ->insert([
        'id' => 1,
        'sku' => 'pencil',
        'price' => 1.29,
    ]);

$item = $storage->table('products')->find(1);

var_dump($item instanceof ItemInterface);
// bool(true)

Documentation

Storages

Pdo MariaDb Storage

use Tobento\Service\Database\PdoDatabaseFactory;
use Tobento\Service\Storage\Tables\Tables;
use Tobento\Service\Storage\PdoMariaDbStorage;
use Tobento\Service\Storage\StorageInterface;
use PDO;

$pdo = (new PdoDatabaseFactory())->createPdo(
    name: 'mysql',
    config: [
        'dsn' => 'mysql:host=localhost;dbname=db_name',
        'username' => 'root',
        'password' => '',
        'options' => [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_EMULATE_PREPARES => false,
        ],
    ],
);

$tables = new Tables();
$tables->add('products', ['id', 'sku', 'price'], 'id');
$tables->add('users', ['id', 'firstname', 'lastname', 'email'], 'id');

$storage = new PdoMariaDbStorage($pdo, $tables);

var_dump($storage instanceof StorageInterface);
// bool(true)

Pdo MySql Storage

use Tobento\Service\Database\PdoDatabaseFactory;
use Tobento\Service\Storage\Tables\Tables;
use Tobento\Service\Storage\PdoMySqlStorage;
use Tobento\Service\Storage\StorageInterface;
use PDO;

$pdo = (new PdoDatabaseFactory())->createPdo(
    name: 'mysql',
    config: [
        'dsn' => 'mysql:host=localhost;dbname=db_name',
        'username' => 'root',
        'password' => '',
        'options' => [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_EMULATE_PREPARES => false,
        ],
    ],
);

$tables = new Tables();
$tables->add('products', ['id', 'sku', 'price'], 'id');
$tables->add('users', ['id', 'firstname', 'lastname', 'email'], 'id');

$storage = new PdoMySqlStorage($pdo, $tables);

var_dump($storage instanceof StorageInterface);
// bool(true)

Json File Storage

use Tobento\Service\Storage\Tables\Tables;
use Tobento\Service\Storage\JsonFileStorage;
use Tobento\Service\Storage\StorageInterface;

$tables = new Tables();
$tables->add('products', ['id', 'sku', 'price'], 'id');
$tables->add('users', ['id', 'firstname', 'lastname', 'email'], 'id');

$storage = new JsonFileStorage(
    dir: 'home/private/storage/',
    tables: $tables
);

var_dump($storage instanceof StorageInterface);
// bool(true)

In Memory Storage

use Tobento\Service\Storage\Tables\Tables;
use Tobento\Service\Storage\InMemoryStorage;
use Tobento\Service\Storage\StorageInterface;

$tables = new Tables();
$tables->add('products', ['id', 'sku', 'price'], 'id');
$tables->add('products_lg', ['product_id', 'language_id', 'title']);
$tables->add('users', ['id', 'firstname', 'lastname', 'email'], 'id');

$storage = new InMemoryStorage([
    'products' => [
        1 => ['id' => 1, 'sku' => 'paper', 'price' => 1.2],
        2 => ['id' => 2, 'sku' => 'pen', 'price' => 1.56],
    ],
    'products_lg' => [
        ['product_id' => 1, 'language_id' => 1, 'title' => 'Papier'],
        ['product_id' => 1, 'language_id' => 2, 'title' => 'Paper'],
        ['product_id' => 2, 'language_id' => 1, 'title' => 'Stift'],
    ],    
    'users' => [
        1 => ['id' => 1, 'firstname' => 'Erika', 'lastname' => 'Mustermann', 'email' => 'erika.mustermann@example.com'],
        2 => ['id' => 2, 'firstname' => 'Mustermann', 'lastname' => 'Mustermann', 'email' => 'mustermann@example.com'],
    ],    
], $tables);

var_dump($storage instanceof StorageInterface);
// bool(true)

Storage Comparison

Return items columns support:

Queries

Select Statements

Retrieving methods

get

Retrieve items.

use Tobento\Service\Storage\ItemsInterface;

$products = $storage->table('products')->get();

var_dump($products instanceof ItemsInterface);
// bool(true)

$products->all();
/*Array
(
    [1] => Array
        (
            [id] => 1
            [sku] => paper
            [price] => 1.2
        )

    [2] => Array
        (
            [id] => 2
            [sku] => pen
            [price] => 1.56
        )

)*/

Check out Items Interface to learn more about it.

column

use Tobento\Service\Storage\ItemInterface;

$column = $storage->table('products')->column('price');

var_dump($column instanceof ItemInterface);
// bool(true)

$column->all();
/*Array
(
    [0] => 1.2
    [1] => 1.56
)*/

Index by a certain column:

$storage->table('products')->column('price', 'sku')->all();
/*Array
(
    [paper] => 1.2
    [pen] => 1.56
)*/

Check out Item Interface to learn more about it.

first

Returns the first found item or NULL.

use Tobento\Service\Storage\ItemInterface;

$product = $storage->table('products')->first();

var_dump($product instanceof ItemInterface);
// bool(true)

$product->all();
/*Array
(
    [id] => 1
    [sku] => paper
    [price] => 1.2
)*/

Check out Item Interface to learn more about it.

find

Returns a single item by id or NULL.

use Tobento\Service\Storage\ItemInterface;

$product = $storage->table('products')->find(2);

var_dump($product instanceof ItemInterface);
// bool(true)

$product->all();
/*Array
(
    [id] => 2
    [sku] => pen
    [price] => 1.56
)*/

Check out Item Interface to learn more about it.

value

Get a single column's value from the first item found.

$value = $storage->table('products')->value('sku');

var_dump($value);
// string(5) "paper"

count

Get the items count.

$count = $storage->table('products')->count();

var_dump($count);
// int(2)

Where Clauses

where / orWhere

$products = $storage->table('products')
    ->where('price', '>', 1.3)
    ->get();
    
$products = $storage->table('products')
    ->where('price', '>', 1.2)
    ->orWhere(function($query) {
        $query->where('price', '>', 1.2)
              ->where('sku', '=', 'pen');
    })
    ->get();
    
$products = $storage->table('products')
    ->where(function($query) {
        $query->where('price', '>', 1.2)
              ->orWhere('sku', '=', 'pen');
    })
    ->get();

// Finds any values that start with "a"
$products = $storage->table('products')
    ->where('sku', 'like', 'a%')
    ->get();
    
// Finds any values that end with "a"
$products = $storage->table('products')
    ->where('sku', 'like', '%a')
    ->get();
    
// Finds any values that have "a" in any position
$products = $storage->table('products')
    ->where('sku', 'like', '%a%')
    ->get();    

Supported operators: =, !=, >, <, >=, <=, <>, <=>, like, not like

whereIn / whereNotIn / orWhereIn / orWhereNotIn

$products = $storage->table('products')
    ->whereIn('id', [2, 3])
    ->get();
    
$products = $storage->table('products')
    ->whereNotIn('id', [2, 3])
    ->get();

whereNull / whereNotNull / orWhereNull / orWhereNotNull

$products = $storage->table('products')
    ->whereNull('price')
    ->get();
    
$products = $storage->table('products')
    ->whereNotNull('price')
    ->get();    

whereBetween / whereNotBetween / orWhereBetween / orWhereNotBetween

$products = $storage->table('products')
    ->whereBetween('price', [1.2, 15])
    ->get();
    
$products = $storage->table('products')
    ->whereNotBetween('price', [1.2, 15])
    ->get();

whereColumn / orWhereColumn

$users = $storage->table('users')
    ->whereColumn('firstname', '=', 'lastname')
    ->get();

Supported operators: =, !=, >, <, >=, <=, <>, <=>

JSON Where Clauses

whereJsonContains / orWhereJsonContains

$products = $storage->table('products')
    ->whereJsonContains('options->color', 'blue')
    ->get();
    
$products = $storage->table('products')
    ->whereJsonContains('options->color', ['blue', 'red'])
    ->get();

whereJsonContainsKey / orWhereJsonContainsKey

$products = $storage->table('products')
    ->whereJsonContainsKey('options->color')
    ->get();

whereJsonLength / orWhereJsonLength

$products = $storage->table('products')
    ->whereJsonLength('options->color', '>', 2)
    ->get();

Supported operators: =, !=, >, <, >=, <=, <>, <=>

Join Clauses

join

$products = $storage->table('products')
    ->join('products_lg', 'id', '=', 'product_id')
    ->get();

leftJoin / rightJoin

$products = $storage->table('products')
    ->leftJoin('products_lg', 'id', '=', 'product_id')
    ->get();
    
$products = $storage->table('products')
    ->rightJoin('products_lg', 'id', '=', 'product_id')
    ->get();

Advanced join clauses

$products = $storage->table('products')
    ->join('products_lg', function($join) {
        $join->on('id', '=', 'product_id')
             ->orOn('id', '=', 'language_id');         
    })
    ->get();
    
$products = $storage->table('products')
    ->join('products_lg', function($join) {
        $join->on('id', '=', 'product_id')
             ->where('product_id', '>', 2);
    })
    ->get();

Group Clauses

$products = $storage->table('products')
    ->groupBy('price')
    ->having('price', '>', 2)
    ->get();
    
$products = $storage->table('products')
    ->groupBy('price')
    ->havingBetween('price', [1, 4])
    ->get();

Select Columns

You may select just specific columns.

$products = $storage->table('products')
    ->select('id', 'sku')
    ->get();
    
$product = $storage->table('products')
    ->select('id', 'sku')
    ->first();

Index Column

Specify the column you want the items to be indexed.

$products = $storage->table('products')
    ->index('sku')
    ->get();

Ordering

$products = $storage->table('products')
    ->order('sku', 'ASC')
    ->get();
    
$products = $storage->table('products')
    ->order('sku', 'DESC')
    ->get();    

Limit

$products = $storage->table('products')
    ->limit(number: 2, offset: 10)
    ->get();

Insert Statements

use Tobento\Service\Storage\ItemInterface;

$insertedItem = $storage
    ->table('products')
    ->insert([
        'sku' => 'glue',
        'price' => 4.55,
    ]);

var_dump($insertedItem instanceof ItemInterface);
// bool(true)

Check out Item Interface to learn more about it.

return specific columns

$insertedItem = $storage
    ->table('products')
    ->insert(
        item: ['sku' => 'glue'],
        return: ['id']
    );

var_dump($insertedItem->all());

return null

$insertedItem = $storage
    ->table('products')
    ->insert(
        item: ['sku' => 'glue'],
        return: null
    );

var_dump($insertedItem->all());
// array(0) { }

Mass Insert Statements

use Tobento\Service\Storage\ItemsInterface;

$insertedItems = $storage
    ->table('products')
    ->insertItems([
        ['sku' => 'glue', 'price' => 4.55],
        ['sku' => 'pencil', 'price' => 1.99],
    ]);

var_dump($insertedItems instanceof ItemsInterface);
// bool(true)

Check out Items Interface to learn more about it.

return specific columns

$insertedItems = $storage
    ->table('products')
    ->insertItems(
        items: [
            ['sku' => 'glue', 'price' => 4.55],
            ['sku' => 'pencil', 'price' => 1.99],
        ],
        return: ['id']
    );

var_dump($insertedItems->all());
// array(2) { [0]=> array(1) { ["id"]=> int(3) } [1]=> array(1) { ["id"]=> int(4) } }

return null

$insertedItems = $storage
    ->table('products')
    ->insertItems(
        items: [
            ['sku' => 'glue', 'price' => 4.55],
            ['sku' => 'pencil', 'price' => 1.99],
        ],
        return: null
    );

var_dump($insertedItems->all());
// array(0) { }
Item Factory

You may use the item factory iterator to seed items and use the Seeder Service to generate fake data.

use Tobento\Service\Iterable\ItemFactoryIterator;
use Tobento\Service\Seeder\Str;
use Tobento\Service\Seeder\Num;

$insertedItems = $storage->table('products')
    ->chunk(length: 20000)
    ->insertItems(
        items: new ItemFactoryIterator(
            function() {
                return [
                    'sku' => Str::string(10),
                    'price' => Num::float(min: 1.5, max: 55.5),
                ];
            },
            create: 1000000 // create 1 million items
        )
    );
    
foreach($insertedItems as $product) {}
Json File Items
use Tobento\Service\Iterable\JsonFileIterator;
use Tobento\Service\Iterable\ModifyIterator;

$iterator = new JsonFileIterator(
    file: 'private/src/products.json',
);

// you may use the modify iterator:
$iterator = new ModifyIterator(
    iterable: $iterator,
    modifier: function(array $item): array {
        return [
          'sku' => $item['sku'] ?? '',
          'price' => $item['price'] ?? '',
        ];
    }
);
        
$insertedItems = $storage->table('products')
    ->chunk(length: 20000)
    ->insertItems($iterator);
    
foreach($insertedItems as $product) {}

Update Statements

You may constrain the update query using where clauses.

use Tobento\Service\Storage\ItemsInterface;

$updatedItems = $storage
    ->table('products')
    ->where('id', '=', 2)
    ->update([
        'price' => 4.55,
    ]);

var_dump($updatedItems instanceof ItemsInterface);
// bool(true)

Check out Items Interface to learn more about it.

return specific columns

$updatedItems = $storage
    ->table('products')
    ->where('price', '>', 1.5)
    ->update(
        item: ['price' => 4.55],
        return: ['id']
    );

var_dump($updatedItems->all());
// array(2) { [0]=> array(1) { ["id"]=> int(2) } }

⚠️ Pdo MySql Storage does not return the items as returning statements are not supported.

You may get the count though:

var_dump($updatedItems->count());
// int(1)

return null

$updatedItems = $storage
    ->table('products')
    ->where('price', '>', 1.5)
    ->update(
        item: ['price' => 4.55],
        return: null
    );

var_dump($updatedItems->all());
// array(0) { }

updateOrInsert

use Tobento\Service\Storage\ItemInterface;
use Tobento\Service\Storage\ItemsInterface;

$items = $storage->table('products')->updateOrInsert(
    ['id' => 2], // where clauses
    ['sku' => 'glue', 'price' => 3.48],
    return: ['id']
);

// if updated:
var_dump($items instanceof ItemsInterface);
// bool(true)

// if inserted:
var_dump($items instanceof ItemInterface);
// bool(true)

Check out Item Interface to learn more about it.

Check out Items Interface to learn more about it.

Updating JSON Columns

$updated = $storage
    ->table('products')
    ->where('id', 2)
    ->update([
        'options->color' => ['red'],
        'options->active' => true,
    ]);

Delete Statements

use Tobento\Service\Storage\ItemsInterface;

$deletedItems = $storage->table('products')
    ->where('price', '>', 1.33)
    ->delete();

var_dump($deletedItems instanceof ItemsInterface);
// bool(true)

Check out Items Interface to learn more about it.

return specific columns

$deletedItems = $storage->table('products')
    ->where('id', '=', 2)
    ->delete(return: ['sku']);

var_dump($deletedItems->all());
// array(1) { [0]=> array(1) { ["sku"]=> string(3) "pen" } }

return null

$deletedItems = $storage->table('products')
    ->where('id', '=', 2)
    ->delete(return: null);

var_dump($deletedItems->all());
// array(0) { }

Transactions

commit

$storage->begin();

// your queries

$storage->commit();

rollback

$storage->begin();

// your queries

$storage->rollback();

transaction

You may use the transaction method to run a set of storage operations within a transaction. If an exception is thrown within the transaction closure, the transaction will automatically be rolled back. If the closure executes successfully, the transaction will automatically be committed.

use Tobento\Service\Storage\StorageInterface;

$storage->transaction(function(StorageInterface $storage) {
    // your queries  
});

Chunking Results and Inserts

You may use the chunk method if you need to work with thousands or even millions of item(s).

column

Returns the column in generator mode.

$column = $storage->table('products')
    ->chunk(length: 2000)
    ->column('price');

foreach($column as $price) {
    var_dump($price);
    // float(1.2)
}

get

Returns the items in generator mode.

$products = $storage->table('products')
    ->chunk(length: 2000)
    ->get();

foreach($products as $product) {
    var_dump($product['sku']);
    // string(5) "paper"
}

insertItems

Returns the inserted items in generator mode.

$insertedItems = $storage
    ->table('products')
    ->chunk(length: 10000)
    ->insertItems([
        ['sku' => 'glue', 'price' => 4.55],
        ['sku' => 'pencil', 'price' => 1.99],
        // ...
    ]);
    
foreach($insertedItems as $product) {
    var_dump($product['id']);
    // int(3)
}

If you set the return parameter to null it will immediately insert the items but not return them in generator mode. You may be able to get the number of items created though.

$insertedItems = $storage
    ->table('products')
    ->chunk(length: 10000)
    ->insertItems([
        ['sku' => 'glue', 'price' => 4.55],
        ['sku' => 'pencil', 'price' => 1.99],
        // ...
    ], return: null);

var_dump($insertedItems->count());
// int(2)

Miscellaneous

new

The new method will return a new storage instance.

$newStorage = $storage->new();

fetchItems

The fetchItems method will return all table items.

$iterable = $storage->fetchItems(table: 'products');

storeItems

The storeItems method will store the items to the table which will be truncated first.

$storedItems = $storage->storeItems(
    table: 'products',
    items: [] // iterable
);

deleteTable

The deleteTable method will delete the table completely.

$storage->deleteTable('products');

Debugging

[$statement, $bindings] = $storage->table('products')
    ->where('id', '=', 1)
    ->getQuery();

var_dump($statement);
// string(56) "SELECT `id`,`sku`,`price` FROM `products` WHERE `id` = ?"

var_dump($bindings);
// array(1) { [0]=> int(1) }

Using a Closure

You must use a Closure if you want to debug other retrieving methods than "get" or insert, update and delete statements.

[$statement, $bindings] = $storage->table('products')
    ->where('id', '=', 1)
    ->getQuery(function(StorageInterface $storage) {
        $storage->update([
            'price' => 4.55,
        ]);
    });

var_dump($statement);
// string(48) "UPDATE `products` SET `price` = ? WHERE `id` = ?"

var_dump($bindings);
// array(2) { [0]=> float(4.55) [1]=> int(1) }

Item Interface

Iterating item attributes:

use Tobento\Service\Storage\ItemInterface;
use Tobento\Service\Storage\Item;

$item = new Item(['title' => 'Title']);

var_dump($item instanceof ItemInterface);
// bool(true)

foreach($item as $attr) {
    var_dump($attr);
    // string(5) "Title"
}

get

Get an item value by key.

use Tobento\Service\Storage\ItemInterface;
use Tobento\Service\Storage\Item;

$item = new Item(['title' => 'Title']);

var_dump($item instanceof ItemInterface);
// bool(true)

var_dump($item->get('title'));
// string(5) "Title"

// returns the default value if the key does not exist.
var_dump($item->get('sku', 'Sku'));
// string(3) "Sku"

all

Returns all items (attributes).

use Tobento\Service\Storage\Item;

$item = new Item(['title' => 'Title']);

var_dump($item->all());
// array(1) { ["title"]=> string(5) "Title" }

count

Returns the number of the attributes.

use Tobento\Service\Storage\Item;

$item = new Item(['title' => 'Title']);

var_dump($item->count());
// int(1)

collection

Returns a new Collection with the attributes.

use Tobento\Service\Storage\Item;
use Tobento\Service\Collection\Collection;

$item = new Item(['title' => 'Title']);

var_dump($item->collection() instanceof Collection);
// bool(true)

Check out the Collection to learn more about it.

Items Interface

Iterating items:

use Tobento\Service\Storage\ItemsInterface;
use Tobento\Service\Storage\Items;

$items = new Items([
    'foo' => ['title' => 'Title'],
]);

var_dump($items instanceof ItemsInterface);
// bool(true)

foreach($items as $item) {
    var_dump($item['title']);
    // string(5) "Title"
}

get

Get an item value by key.

use Tobento\Service\Storage\Items;

$items = new Items([
    'foo' => ['title' => 'Title'],
]);

var_dump($items->get('foo.title'));
// string(5) "Title"

// returns the default value if the key does not exist.
var_dump($items->get('foo.sku', 'Sku'));
// string(3) "Sku"

all

Returns all items.

use Tobento\Service\Storage\Items;

$items = new Items([
    'foo' => ['title' => 'Title'],
]);

var_dump($items->all());
// array(1) { ["foo"]=> array(1) { ["title"]=> string(5) "Title" } }

first

Returns the first item, otherwise null.

use Tobento\Service\Storage\Items;

$items = new Items([
    ['foo' => 'Foo'],
    ['bar' => 'Bar'],
]);

var_dump($items->first());
// array(1) { ["foo"]=> string(3) "Foo" }

column

Returns the column of the items.

use Tobento\Service\Storage\Items;

$items = new Items([
    ['name' => 'Foo', 'id' => 3],
    ['name' => 'Bar', 'id' => 5],
]);

var_dump($items->column(column: 'name'));
// array(2) { [0]=> string(3) "Foo" [1]=> string(3) "Bar" }

// with index
var_dump($items->column(column: 'name', index: 'id'));
// array(2) { [3]=> string(3) "Foo" [5]=> string(3) "Bar" }

map

Map over each of the items returning a new instance.

use Tobento\Service\Storage\Items;
use Tobento\Service\Storage\Item;

$items = new Items([
    ['foo' => 'Foo'],
    ['bar' => 'Bar'],
]);

$itemsNew = $items->map(function(array $item): object {
    return new Item($item);
});

var_dump($itemsNew->first());
// object(Tobento\Service\Storage\Item)#8 ...

groupBy

Returns a new instance with the grouped items.

use Tobento\Service\Storage\Items;

$items = new Items([
    ['name' => 'bear', 'group' => 'animals'],
    ['name' => 'audi', 'group' => 'cars'],
    ['name' => 'ant', 'group' => 'animals'],
]);

$groups = $items->groupBy(groupBy: 'group')->all();
/*Array
(
    [animals] => Array
        (
            [0] => Array
                (
                    [name] => bear
                    [group] => animals
                )
            [2] => Array
                (
                    [name] => ant
                    [group] => animals
                )
        )
    [cars] => Array
        (
            [1] => Array
                (
                    [name] => audi
                    [group] => cars
                )
        )
)*/

// using a callable:
$groups = $items->groupBy(
    groupBy: fn ($item) => $item['group'],
);

// using a callable for grouping:
$groups = $items->groupBy(
    groupBy: 'group',
    groupAs: fn (array $group) => (object) $group,
);

// without preserving keys:
$groups = $items->groupBy(
    groupBy: 'group',
    preserveKeys: false,
);

reindex

Reindex items returning a new instance.

use Tobento\Service\Storage\Items;

$items = new Items([
    ['sku' => 'foo', 'name' => 'Foo'],
    ['sku' => 'bar', 'name' => 'Bar'],
]);

$itemsNew = $items->reindex(function(array $item): int|string {
    return $item['sku'];
});

var_dump(array_keys($itemsNew->all()));
// array(2) {[0]=> string(3) "foo" [1]=> string(3) "bar"}

count

Returns the number of the items.

use Tobento\Service\Storage\Items;

$items = new Items([
    'foo' => ['title' => 'Title'],
]);

var_dump($items->count());
// int(1)

collection

Returns a new Collection with the items.

use Tobento\Service\Storage\Items;
use Tobento\Service\Collection\Collection;

$items = new Items([
    'foo' => ['title' => 'Title'],
]);

var_dump($items->collection() instanceof Collection);
// bool(true)

Check out the Collection to learn more about it.

Tables

Tables are used by the storages for verifying table and column names for building queries.

add

use Tobento\Service\Storage\Tables\Tables;

$tables = new Tables();
$tables->add(
    table: 'products',
    columns: ['id', 'sku'],
    primaryKey: 'id' // or null if none
);

Credits