PHPackages                             pnoexz/spot3 - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. pnoexz/spot3

ActiveLibrary[Database &amp; ORM](/categories/database)

pnoexz/spot3
============

Fork of Spot2: a simple DataMapper built on top of Doctrine DBAL

0.0.6(7y ago)067BSD-3-ClausePHPPHP &gt;=7.1

Since Apr 2Pushed 7y ago1 watchersCompare

[ Source](https://github.com/Pnoexz/spot3)[ Packagist](https://packagist.org/packages/pnoexz/spot3)[ RSS](/packages/pnoexz-spot3/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (4)Versions (10)Used By (0)

Spot DataMapper ORM v2.0 [![Build Status](https://camo.githubusercontent.com/1ae3306169e78cf6d2901db6070120e3e8748bce88da8b1d87ecf945c7919d0e/68747470733a2f2f7472617669732d63692e6f72672f73706f746f726d2f73706f74322e737667)](https://travis-ci.org/spotorm/spot2)
===================================================================================================================================================================================================================================================================

[](#spot-datamapper-orm-v20-)

Spot v2.x is built on the [Doctrine DBAL](http://www.doctrine-project.org/projects/dbal.html), and targets PHP 5.4+.

The aim of Spot is to be a lightweight DataMapper alternative that is clear, efficient, and simple - and doesn't use annotations or proxy classes.

Using Spot In Your Project
--------------------------

[](#using-spot-in-your-project)

Spot is a standalone ORM that can be used in any project. Follow the instructions below to get Spot setup in your project.

Installation with Composer
--------------------------

[](#installation-with-composer)

```
composer require vlucas/spot2
```

Connecting to a Database
------------------------

[](#connecting-to-a-database)

The `Spot\Locator` object is the main point of access to spot that you will have to be able to access from everywhere you need to run queries or work with your entities. It is responsible for loading mappers and managing configuration. To create a Locator, you will need a `Spot\Config` object.

The `Spot\Config` object stores and references database connections by name. Create a new instance of `Spot\Config` and add database connections with DSN strings so Spot can establish a database connection, then create your locator object:

```
$cfg = new \Spot\Config();

// MySQL
$cfg->addConnection('mysql', 'mysql://user:password@localhost/database_name');
// Sqlite
$cfg->addConnection('sqlite', 'sqlite://path/to/database.sqlite');

$spot = new \Spot\Locator($cfg);
```

You can also use [DBAL-compatible configuration arrays](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html)instead of DSN strings if you prefer:

```
$cfg->addConnection('mysql', [
    'dbname' => 'mydb',
    'user' => 'user',
    'password' => 'secret',
    'host' => 'localhost',
    'driver' => 'pdo_mysql',
]);
```

Accessing the Locator
---------------------

[](#accessing-the-locator)

Since you have to have access to your mapper anywhere you use the database, most people create a helper method to create a mapper instance once and then return the same instance when required again. Such a helper method might look something like this:

```
function spot() {
    static $spot;
    if($spot === null) {
        $spot = new \Spot\Locator();
        $spot->config()->addConnection('test_mysql', 'mysql://user:password@localhost/database_name');
    }
    return $spot;
}
```

If you are using a framework with a dependency injection container or service, you will want to use it so that the `Spot\Locator` object is available everywhere in your application that you need it.

Getting A Mapper
----------------

[](#getting-a-mapper)

Since Spot follows the DataMapper design pattern, you will need a mapper instance for working with object Entities and database tables. You can get a mapper instance from the `Spot\Locator` object's `mapper` method by providing the fully qualified entity namespace + class name:

```
$postMapper = $spot->mapper('Entity\Post');
```

Mappers only work with one entity type, so you will need one mapper per entity class you work with (i.e. to save an Entity\\Post, you will need the appropriate mapper, and to save an Entity\\Comment, you will need a comment mapper, not the same post mapper. Relations will automatically be loaded and handled by their corresponding mapper by Spot.

**NOTE: You do NOT have to create a mapper for each entity unless you need custom finder methods or other custom logic. If there is no entity-specific mapper for the entity you want, Spot will load the generic mapper for you and return it.**

Creating Entities
-----------------

[](#creating-entities)

Entity classes can be named and namespaced however you want to set them up within your project structure. For the following examples, the Entities will just be prefixed with an `Entity` namespace for easy psr-0 compliant autoloading.

```
namespace Entity;

use Spot\EntityInterface as Entity;
use Spot\MapperInterface as Mapper;

class Post extends \Spot\Entity
{
    protected static $table = 'posts';

    public static function fields()
    {
        return [
            'id'           => ['type' => 'integer', 'autoincrement' => true, 'primary' => true],
            'title'        => ['type' => 'string', 'required' => true],
            'body'         => ['type' => 'text', 'required' => true],
            'status'       => ['type' => 'integer', 'default' => 0, 'index' => true],
            'author_id'    => ['type' => 'integer', 'required' => true],
            'date_created' => ['type' => 'datetime', 'value' => new \DateTime()]
        ];
    }

    public static function relations(Mapper $mapper, Entity $entity)
    {
        return [
            'tags' => $mapper->hasManyThrough($entity, 'Entity\Tag', 'Entity\PostTag', 'tag_id', 'post_id'),
            'comments' => $mapper->hasMany($entity, 'Entity\Post\Comment', 'post_id')->order(['date_created' => 'ASC']),
            'author' => $mapper->belongsTo($entity, 'Entity\Author', 'author_id')
        ];
    }
}
```

Using Custom Mappers
--------------------

[](#using-custom-mappers)

Although you do not have to create a mapper for each entity, sometimes it is nice to create one if you have a lot of custom finder methods, or want a better place to contain the logic of building all the queries you need.

Just specify the full mapper class name in your entity:

```
namespace Entity;

class Post extends \Spot\Entity
{
    protected static $mapper = 'Entity\Mapper\Post';

    // ... snip ...
}
```

And then create your mapper:

```
namespace Entity\Mapper;

use Spot\Mapper;

class Post extends Mapper
{
    /**
     * Get 10 most recent posts for display on the sidebar
     *
     * @return \Spot\Query
     */
    public function mostRecentPostsForSidebar()
    {
        return $this->where(['status' => 'active'])
            ->order(['date_created' => 'DESC'])
            ->limit(10);
    }
}
```

Then when you load the mapper like normal, Spot will see the custom `Entity\Post::$mapper` you defined, and load that instead of the generic one, allowing you to call your custom method:

```
$mapper = $spot->mapper('Entity\Post');
$sidebarPosts = $mapper->mostRecentPostsForSidebar();
```

Field Types
-----------

[](#field-types)

Since Spot v2.x is built on top of DBAL, all the [DBAL types](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html)are used and fully supported in Spot:

Integer Types

- `smallint`
- `integer`
- `bigint`

Decimal Types

- `decimal`
- `float`

String Types

- `string`
- `text`
- `guid`

Binary String Types

- `binary`
- `blob`

Boolean/Bit Types

- `boolean`

Date and Time Types

- `date`
- `datetime`
- `datetimetz`
- `time`

Array Types

- `array` - PHP serialize/deserialze
- `simple_array` - PHP implode/explode
- `json_array` - json\_encode/json\_decode

Object Types

- `object` - PHP serialize/deserialze

Please read the [Doctrine DBAL Types Reference Page](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html)thoroughly for more information and types and cross-database support. Some types may be stored differently on different databases, depending on database vendor support and other factors.

#### Registering Custom Field Types

[](#registering-custom-field-types)

If you want to register your own custom field type with custom functionality on get/set, have a look at the [Custom Mapping Types on the DBAL reference page](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types).

Since Spot uses the DBAL internally, there are no additional changes you have to make for your custom type to work with Spot.

Migrations / Creating and Updating Tables
-----------------------------------------

[](#migrations--creating-and-updating-tables)

Spot comes with a method for running migrations on Entities that will automatically CREATE and ALTER tables based on the current Entity's `fields`definition.

```
$mapper = $spot->mapper('Entity\Post');
$mapper->migrate();
```

Your database should now have the `posts` table in it, with all the fields you described in your `Post` entity.

**NOTE: Please note that re-naming columns is not supported in migrations because there is no way for spot to know which column you renamed to what - Spot will see a new column that needs to be created, and a column that no longer exists and needs to be dropped. This could result in data loss during an auto-migration.**

Finders (Mapper)
----------------

[](#finders-mapper)

The main finders used most are `all` to return a collection of entities, and `first` or `get` to return a single entity matching the conditions.

### all()

[](#all)

Find all entities and return a `Spot\Entity\Collection` of loaded `Spot\Entity`objects.

### where(\[conditions\])

[](#whereconditions)

Find all entities that match the given conditions and return a `Spot\Entity\Collection` of loaded `Spot\Entity` objects.

```
// Where can be called directly from the mapper
$posts = $mapper->where(['status' => 1]);

// Or chained using the returned `Spot\Query` object - results identical to above
$posts = $mapper->all()->where(['status' => 1]);

// Or more explicitly using using `select`, which always returns a `Spot\Query` object
$posts = $mapper->select()->where(['status' => 1]);
```

Since a `Spot\Query` object is returned, conditions and other statements can be chained in any way or order you want. The query will be lazy-executed on interation or `count`, or manually by ending the chain with a call to `execute()`.

### first(\[conditions\])

[](#firstconditions)

Find and return a single `Spot\Entity` object that matches the criteria.

```
$post = $mapper->first(['title' => "Test Post"]);
```

Or `first` can be used on a previous query with `all` to fetch only the first matching record.

```
$post = $mapper->all(['title' => "Test Post"])->first();
```

A call to `first` will always execute the query immediately, and return either a single loaded entity object, or boolean `false`.

### Conditional Queries

[](#conditional-queries)

```
# All posts with a 'published' status, descending by date_created
$posts = $mapper->all()
    ->where(['status' => 'published'])
    ->order(['date_created' => 'DESC']);

# All posts that are not published
$posts = $mapper->all()
    ->where(['status ' => 'published'])

# All posts created before 3 days ago
$posts = $mapper->all()
    ->where(['date_created query("SELECT * FROM posts WHERE id = 1");
```

#### Using Query Parameters

[](#using-query-parameters)

```
$posts = $mapper->query("SELECT * FROM posts WHERE id = ?", [1]);
```

#### Using Named Placeholders

[](#using-named-placeholders)

```
$posts = $mapper->query("SELECT * FROM posts WHERE id = :id", ['id' => 1]);
```

**NOTE: Spot will load ALL returned columns on the target entity from the query you run. So if you perform a JOIN or get more data than the target entity normally has, it will just be loaded on the target entity, and no attempt will be made to map the data to other entities or to filter it based on only the defined fields.**

Relations
---------

[](#relations)

Relations are convenient ways to access related, parent, and child entities from another loaded entity object. An example might be `$post->comments` to query for all the comments related to the current `$post` object.

### Live Query Objects

[](#live-query-objects)

All relations are returned as instances of relation classes that extend `Spot\Relation\RelationAbstract`. This class holds a `Spot\Query` object internally, and allows you to chain your own query modifications on it so you can do custom things with relations, like ordering, adding more query conditions, etc.

```
$mapper->hasMany($entity, 'Entity\Comment', 'post_id')
    ->where(['status' => 'active'])
    ->order(['date_created' => 'ASC']);
```

All of these query modifications are held in a queue, and are run when the relation is actually executed (on `count` or `foreach` iteration, or when `execute` is explicitly called).

### Eager Loading

[](#eager-loading)

All relation types are lazy-loaded by default, and can be eager-loaded to solve the N+1 query problem using the `with` method:

```
$posts = $posts->all()->with('comments');
```

Multiple relations can be eager-loaded using an array:

```
$posts = $posts->all()->with(['comments', 'tags']);
```

### Relation Types

[](#relation-types)

Entity relation types are:

- `HasOne`
- `BelongsTo`
- `HasMany`
- `HasManyThrough`

### HasOne

[](#hasone)

HasOne is a relation where the *related object has a field which points to the current object* - an example might be `User` has one `Profile`.

#### Method

[](#method)

```
$mapper->hasOne(Entity $entity, $foreignEntity, $foreignKey)
```

- `$entity` - The current entity instance
- `$foreignEntity` - Name of the entity you want to load
- `$foreignKey` - Field name on the `$foreignEntity` that matches up with the primary key of the current entity

#### Example

[](#example)

```
namespace Entity;

use Spot\EntityInterface as Entity;
use Spot\MapperInterface as Mapper;

class User extends \Spot\Entity
{
    protected static $table = 'users';

    public static function fields()
    {
        return [
            'id'           => ['type' => 'integer', 'autoincrement' => true, 'primary' => true],
            'username'     => ['type' => 'string', 'required' => true],
            'email'        => ['type' => 'string', 'required' => true],
            'status'       => ['type' => 'integer', 'default' => 0, 'index' => true],
            'date_created' => ['type' => 'datetime', 'value' => new \DateTime()]
        ];
    }

    public static function relations(Mapper $mapper, Entity $entity)
    {
        return [
            'profile' => $mapper->hasOne($entity, 'Entity\User\Profile', 'user_id')
        ];
    }
}
```

In this scenario, the `Entity\User\Profile` entity has a field named `user_id`which the `Entity\User`'s `id` field as a value. Note that *no field exists on this entity for this relation, but rather the related entity*.

### BelongsTo

[](#belongsto)

BelongsTo is a relation where the *current object has a field which points to the related object* - an example might be `Post` belongs to `User`.

#### Method

[](#method-1)

```
$mapper->belongsTo(Entity $entity, $foreignEntity, $localKey)
```

- `$entity` - The current entity instance
- `$foreignEntity` - Name of the entity you want to load
- `$localKey` - Field name on the current entity that matches up with the primary key of `$foreignEntity` (the one you want to load)

#### Example

[](#example-1)

```
namespace Entity;

use Spot\EntityInterface as Entity;
use Spot\MapperInterface as Mapper;

class Post extends \Spot\Entity
{
    protected static $table = 'posts';

    public static function fields()
    {
        return [
            'id'           => ['type' => 'integer', 'autoincrement' => true, 'primary' => true],
            'user_id'      => ['type' => 'integer', 'required' => true],
            'title'        => ['type' => 'string', 'required' => true],
            'body'         => ['type' => 'text', 'required' => true],
            'status'       => ['type' => 'integer', 'default' => 0, 'index' => true],
            'date_created' => ['type' => 'datetime', 'value' => new \DateTime()]
        ];
    }

    public static function relations(Mapper $mapper, Entity $entity)
    {
        return [
            'user' => $mapper->belongsTo($entity, 'Entity\User', 'user_id')
        ];
    }
}
```

In this scenario, the `Entity\Post` entity has a field named `user_id` which is the `Entity\User`'s `id` field's value. Note that *the field exists on this entity for this relation, but not on the related entity*.

### HasMany

[](#hasmany)

HasMany is used where a single record relates to multiple other records - an example might be `Post` has many `Comments`.

#### Method

[](#method-2)

```
$mapper->hasMany(Entity $entity, $entityName, $foreignKey, $localValue = null)
```

- `$entity` - The current entity instance
- `$entityName` - Name of the entity you want to load a collection of
- `$foreignKey` - Field name on the `$entityName` that matches up with the current entity's primary key

#### Example

[](#example-2)

We start by adding a `comments` relation to our `Post` object:

```
namespace Entity;

use Spot\EntityInterface as Entity;
use Spot\MapperInterface as Mapper;

class Post extends Spot\Entity
{
    protected static $table = 'posts';

    public static function fields()
    {
        return [
            'id'           => ['type' => 'integer', 'autoincrement' => true, 'primary' => true],
            'title'        => ['type' => 'string', 'required' => true],
            'body'         => ['type' => 'text', 'required' => true],
            'status'       => ['type' => 'integer', 'default' => 0, 'index' => true],
            'date_created' => ['type' => 'datetime', 'value' => new \DateTime()]
        ];
    }

    public static function relations(Mapper $mapper, Entity $entity)
    {
        return [
            'comments' => $mapper->hasMany($entity, 'Entity\Comment', 'post_id')->order(['date_created' => 'ASC']),
        ];
    }
}
```

And add a `Entity\Post\Comment` object with a 'belongsTo' relation back to the post:

```
namespace Entity;

class Comment extends \Spot\Entity
{
    // ... snip ...

    public static function relations(Mapper $mapper, Entity $entity)
    {
        return [
            'post' => $mapper->belongsTo($entity, 'Entity\Post', 'post_id')
        ];
    }
}
```

### HasManyThrough

[](#hasmanythrough)

HasManyThrough is used for many-to-many relationships. An good example is tagging. A post has many tags, and a tag has many posts. This relation is a bit more complex than the others, because a HasManyThrough requires a join table and mapper.

#### Method

[](#method-3)

```
$mapper->hasManyThrough(Entity $entity, string $hasManyEntity, string $throughEntity, string $selectField, string $whereField)
```

- `$entity` - The current entity instance
- `$hasManyEntity` - This is the target entity you want a collection of. In this case, we want a collection of `Entity\Tag` objects.
- `$throughEntity` - Name of the entity we are going through to get what we want - In this case, `Entity\PostTag`.
- `$selectField` - Name of the field on the `$throughEntity` that will select records by the primary key of `$hasManyEntity`.
- `$whereField` - Name of the field on the `$throughEntity` to select records by the current entities' primary key (we have a post, so this will be the `Entity\PostTag->post_id` field).

#### Example

[](#example-3)

We need to add the `tags` relation to our `Post` entity, specifying query conditions for both sides of the relation.

```
namespace Entity;

use Spot\EntityInterface as Entity;
use Spot\MapperInterface as Mapper;

class Post extends Spot\Entity
{
    protected static $table = 'posts';

    public static function fields()
    {
        return [
            'id'           => ['type' => 'integer', 'autoincrement' => true, 'primary' => true],
            'title'        => ['type' => 'string', 'required' => true],
            'body'         => ['type' => 'text', 'required' => true],
            'status'       => ['type' => 'integer', 'default' => 0, 'index' => true],
            'date_created' => ['type' => 'datetime', 'value' => new \DateTime()]
        ];
    }

    public static function relations(Mapper $mapper, Entity $entity)
    {
        return [
            'tags' => $mapper->hasManyThrough($entity, 'Entity\Tag', 'Entity\PostTag', 'tag_id', 'post_id'),
        ];
    }
```

#### Explanation

[](#explanation)

The result we want is a collection of `Entity\Tag` objects where the id equals the `post_tags.tag_id` column. We get this by going through the `Entity\PostTags` entity, using the current loaded post id matching `post_tags.post_id`.

###  Health Score

27

—

LowBetter than 49% of packages

Maintenance20

Infrequent updates — may be unmaintained

Popularity8

Limited adoption so far

Community20

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 55.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Total

6

Last Release

2575d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/d3860283f4d1eeec30edc7b05a9a6063a5a5143dd0a13b197fdc54a45e292047?d=identicon)[Pnoexz](/maintainers/Pnoexz)

---

Top Contributors

[![vlucas](https://avatars.githubusercontent.com/u/187?v=4)](https://github.com/vlucas "vlucas (167 commits)")[![FlipEverything](https://avatars.githubusercontent.com/u/2407139?v=4)](https://github.com/FlipEverything "FlipEverything (20 commits)")[![nebulousGirl](https://avatars.githubusercontent.com/u/1899256?v=4)](https://github.com/nebulousGirl "nebulousGirl (17 commits)")[![localheinz](https://avatars.githubusercontent.com/u/605483?v=4)](https://github.com/localheinz "localheinz (12 commits)")[![Pnoexz](https://avatars.githubusercontent.com/u/3297901?v=4)](https://github.com/Pnoexz "Pnoexz (11 commits)")[![Jasrags](https://avatars.githubusercontent.com/u/131606?v=4)](https://github.com/Jasrags "Jasrags (10 commits)")[![jakefolio](https://avatars.githubusercontent.com/u/199860?v=4)](https://github.com/jakefolio "jakefolio (9 commits)")[![willemwollebrants](https://avatars.githubusercontent.com/u/916958?v=4)](https://github.com/willemwollebrants "willemwollebrants (8 commits)")[![Dijky](https://avatars.githubusercontent.com/u/9408779?v=4)](https://github.com/Dijky "Dijky (7 commits)")[![tuupola](https://avatars.githubusercontent.com/u/21913?v=4)](https://github.com/tuupola "tuupola (6 commits)")[![marcojetson](https://avatars.githubusercontent.com/u/408194?v=4)](https://github.com/marcojetson "marcojetson (5 commits)")[![greydnls](https://avatars.githubusercontent.com/u/1276798?v=4)](https://github.com/greydnls "greydnls (4 commits)")[![dasper](https://avatars.githubusercontent.com/u/976723?v=4)](https://github.com/dasper "dasper (3 commits)")[![calummackenzie](https://avatars.githubusercontent.com/u/1638217?v=4)](https://github.com/calummackenzie "calummackenzie (2 commits)")[![joecwallace](https://avatars.githubusercontent.com/u/925701?v=4)](https://github.com/joecwallace "joecwallace (2 commits)")[![rskuipers](https://avatars.githubusercontent.com/u/1918518?v=4)](https://github.com/rskuipers "rskuipers (2 commits)")[![kitchenu](https://avatars.githubusercontent.com/u/19553191?v=4)](https://github.com/kitchenu "kitchenu (2 commits)")[![splio-aleroux](https://avatars.githubusercontent.com/u/5426731?v=4)](https://github.com/splio-aleroux "splio-aleroux (2 commits)")[![jwage](https://avatars.githubusercontent.com/u/97422?v=4)](https://github.com/jwage "jwage (1 commits)")[![bikalbasnet](https://avatars.githubusercontent.com/u/7262965?v=4)](https://github.com/bikalbasnet "bikalbasnet (1 commits)")

---

Tags

databaseormdoctrinedbalmodelentitymapperdatamapper

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/pnoexz-spot3/health.svg)

```
[![Health](https://phpackages.com/badges/pnoexz-spot3/health.svg)](https://phpackages.com/packages/pnoexz-spot3)
```

###  Alternatives

[vlucas/spot2

Simple DataMapper built on top of Doctrine DBAL

605392.8k7](/packages/vlucas-spot2)[scienta/doctrine-json-functions

A set of extensions to Doctrine that add support for json query functions.

58523.9M36](/packages/scienta-doctrine-json-functions)[jsor/doctrine-postgis

Spatial and Geographic Data with PostGIS and Doctrine.

2191.6M1](/packages/jsor-doctrine-postgis)[williarin/wordpress-interop

Interoperability library to work with WordPress database in third party apps

6610.9k2](/packages/williarin-wordpress-interop)[andsalves/doctrine-elastic

Elasticsearch Doctrine Adaptation

156.6k](/packages/andsalves-doctrine-elastic)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
