Android Room Generic DAO - java

Good day Stack, i'm working on an Android project that uses Android's Room 1.0.0 Alpha 5, the main issue that i'm facing is that every time i need to call one of the DAO from room i need to do something like this:
Activity.java:
...
AppDatabase db = Room.databaseBuilder(context, AppDatabase.class, "Storage").build();
Table1 table = new Table1();
table.setId(1);
table.setName("Hello");
new AccessDB().execute(1);
/* Generic AccessDB needed */
private class AccessDB extends AsyncTask<Integer,Void,List<Table1>> {
#Override
protected List<Table1> doInBackground(Integer... param) {
switch(param[0]) {
case 1:
return db.Table1DAO().create();
case 2:
return db.Table1DAO().read();
}
return new ArrayList<>();
}
#Override
protected void onPostExecute(List<Table1> list) {
processData(list);
}
}
...
I know that i can access Room DB from the main thread, and that would shrink the code, but i think that's not a good practice since that would lock the activity every time it has to handle data.
So if i need to insert or read data from "Table2" i would have to do the same all over again, it would be great if i could turn the entity types into generics like "T" or something like that and then make a generic "AccessDB".
But since i'm not too familiar with Java... I'm currently struggling with this.
Here is some other code of the instances.
AppDatabase.java:
#Database(entities = {Table1.class, Table2.class, Table3.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract Table1DAO Table1DAO();
public abstract Table2DAO Table2DAO();
public abstract Table3DAO Table3DAO();
}
Table1.java:
#Entity
public class Table1 {
/* setters & getters */
#PrimaryKey(autoGenerate = true)
private int id;
private String name;
}
Table1DAO.java:
#Dao public interface Table1DAO {
#Query("SELECT * FROM Table1")
List<Table1> read(Table1 table);
#Insert(onConflict = OnConflictStrategy.REPLACE)
List<Long> create(Table1... table);
}
Thank you all for your help.

You can use inheritance and create a BaseDao which will be implemented by all your child Dao. This way you won't need to write the common methods again and again.
interface BaseDao<T> {
/**
* Insert an object in the database.
*
* #param obj the object to be inserted.
*/
#Insert
fun insert(obj: T)
/**
* Insert an array of objects in the database.
*
* #param obj the objects to be inserted.
*/
#Insert
fun insert(vararg obj: T)
/**
* Update an object from the database.
*
* #param obj the object to be updated
*/
#Update
fun update(obj: T)
/**
* Delete an object from the database
*
* #param obj the object to be deleted
*/
#Delete
fun delete(obj: T)
}
Read more about it: https://gist.github.com/florina-muntenescu/1c78858f286d196d545c038a71a3e864#file-basedao-kt
Original credits to Florina.

I played around a bit with the answer of Akshay Chordiya, but needed two additions:
ability to insert/update List
return values to monitor insert/update success
Here is what I came up with:
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Update
/**
* List of all generic DB actions
* All use suspend to force kotlin coroutine usage, remove if not required
*/
#Dao
interface BaseDao<T> {
// insert single
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(obj: T?): Long
// insert List
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(obj: List<T>?) : List<Long>
// update List
#Update
suspend fun update(obj: List<T>?): Int
}
#Dao
interface MyObjectDao : BaseDao<MyObject> {
#Query("SELECT * from $TABLE_NAME WHERE $COL_ID = :id")
suspend fun getById(id: Long): MyObject
}
Can then be called like:
val ids = MyObjectDao.insert(objectList)

Related

How to check for success/failure in Java Spring database queries?

I have some code to delete a recipe from my mongoDB database:
#RequestMapping(value = "/recipe/{id}", method = RequestMethod.DELETE)
public String deleteSingleRecipe(#PathVariable("id") String recipeId) {
try {
repository.deleteById(recipeId);
return "Deleted RECIPE success";
} catch (Exception ex) {
return ex.toString();
}
}
This is able to successfully delete a recipe based on ID. However, I'm unsure how to catch cases such as if the recipe doesn't even exist, or if the deletion is failed.
With JavaScript/Node this was really easy because I could pass a callback function with depending on if result/error were null I could determine the success of the query and proceed. I'm pretty lost how to do this in Java/Spring.
When I tried to delete a recipe a 2nd time I still got "Deleted RECIPE success".
If you check the JPARepository Interface you will get
/**
* Deletes the entity with the given id.
*
* #param id must not be {#literal null}.
* #throws IllegalArgumentException in case the given {#code id} is {#literal null}
*/
void deleteById(ID id);
/**
* Deletes a given entity.
*
* #param entity
* #throws IllegalArgumentException in case the given entity is {#literal null}.
*/
void delete(T entity);
so as per your requirement it will not throw any exception if given id is not exist in DB.
for that you can use boolean isFound = repository.existsById(recipeId); and if isFound is true you can delete it. and if isFound is false then you can throw exception.
second way is you can check
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T>
this class contains deleteById method. and this method will throw exception if id is not exist in DB.
/*
* (non-Javadoc)
* #see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable)
*/
#Transactional
public void deleteById(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(
String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1)));
}
Starting from Spring Data JPA (>=1.7.x), we can use the derived delete or remove query. It returns the number of entities deleted or what all entities are deleted.
Using it you can update your code as:
public interface RecipetRepository extends CrudRepository<Reciepe, Long> {
long deleteById(String id);
}
and we can throw an exception if the entity doesn't exist.
#RequestMapping(value = "/recipe/{id}", method = RequestMethod.DELETE)
public String deleteSingleRecipe(#PathVariable("id") String recipeId) {
long numberOfRecipiesDeleted;
try {
numberOfRecipiesDeleted = repository.deleteById(recipeId);
} catch (DataAccessException ex) {
return ex.toString();
}
if(numberOfRecipiesDeleted == 0){
throw new ReciepeNotFoundException();
}
return "Deleted RECIPE success";
}
You can add a check before using existsById
boolean isFound = repository.existsById(recipeId);
Returns whether an entity with the given id exists.
Try add a new method to the repository interface like this
#Modifying
#Query("DELETE FROM MY_TABLE WHERE ID = ?1")
default boolean deleteById(IdPrimitiveType id){
return true;
}
Hope it helps
It has been reported to Spring as a bug (which is still open) : https://jira.spring.io/browse/DATAMONGO-1997?jql=text%20~%20%22deleteById%22
Checking if it exists before or after was not a solution for us as it is sensitive to race condition. The best and more efficient workaround we found was what #SandOfTime suggested : in your MongoRepository interface, add :
#DeleteQuery("{_id:?0}")
Long deleteByIdReturningDeletedCount(String id);
I also had a similar kind of requirement but my case is slightly different. First I'll tell my requirement I need to add a user if the username is not exist. So in the Repository there is a built in method to find the username.
Usermodel findByUsername(String username);
So in the Controller layer I've implemented the following.
public String signUp(#RequestBody Usermodel user) {
if(userRepository.findByUsername(user.getUsername())==null) {
userRepository.save(user);
return "Success";
}
else{
return "Username Already Exsist";
}
}
I think for your case also this can be applied. Need to check the recipeId and then you can perform a deletion.

Android : How to OnConflictStrategy.REPLACE but preserve one particular field

I have a DAO in Android Room, with OnConflictStrategy.REPLACE on insert, there's a boolean field downloaded, which gets changed to true if this object was downloaded by the user, I want to replace the whole object onConflict, but preserve the state of this field(downloaded) in the db.
public interface DAOTemplate<T> {
#Insert(onConflict = OnConflictStrategy.REPLACE)
#NonNull
void insert(T... messages);
#Delete
void delete(T message);
#Update
void update(T message);
}
A bit late for the answer but I just did the same in my code. Your approach should be not to replace the object but update the existing one with the fields you need to update and just keep the downloaded.
For this use OnConflictStrategy.FAIL. Surround your insert() with try/catch block and then in catch put your update() call. I am using RXJava + Kotlin to so but you can implement this logic with any other async approach.
fun saveItem(item: Item): Single<Unit>? =
Single.fromCallable {
try {
dao.save(item)
} catch (exception: SQLiteConstraintException) {
dao.updateModel(item.id)
} catch (throwable: Throwable) {
dao.updateModel(item.id)
}
}.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread())
And in DAO I am implementing update as follows:
#Query("UPDATE name_of_your_table SET quantity = quantity + 1 WHERE id = :id")
fun updateModel(id: String)
Here I am increasing the quantity but you can pass fields to update as params and use SET to set all fields you need to update.
I also needed a solution to preserve some fields when updating room db. Thank you very much #Rainmaker, I extended #Rainmaker solution. I know it is too late but I want to drop my solution here for future needs.
override suspend fun upsertItem(item: YourItem) {
try{
itemDao.insertItem(item)
}catch (exception: SQLiteConstraintException){
val oldItem = itemDao.getItemByIdOneShot(item.id)
itemDao.updateItem(item.apply {
downloaded = oldItem.downloaded
// you can add more fields here
})
}catch (throwable: Throwable){
val oldItem = itemDao.getItemByIdOneShot(item.id)
itemDao.updateItem(item.apply {
downloaded = oldItem.downloaded
// you can add more fields here
})
}
}
Here is my Dao
#Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insertItem(item: YourItem)
#Update
abstract suspend fun updateItem(item: YourItem)
#Query("select * from items where id= :id")
abstract fun getItemByIdOneShot(id: Int): YourItem

Room doesn't store data

My aim is to store some data into a SQLite database using Room.
So I made a lot of #Entities in POJO.
For each #Entity I made a #Dao with at least these queries:
#Dao
public interface RouteDao {
#Query("SELECT * FROM route")
LiveData<List<Route>> getAll();
#Insert
void insertAll(List<Route> routes);
#Query("DELETE FROM route")
void deleteAll();
}
My Singleton Room #Database is:
#Database(entities = {Agency.class, Calendar.class, CalendarDate.class, FeedInfo.class, Route.class, Stop.class, StopTime.class, Transfer.class, Trip.class}, version = 1)
#TypeConverters(MyConverters.class)
public abstract class GtfsDatabase extends RoomDatabase {
private static final String DATABASE_NAME = "gtfs-db";
private static GtfsDatabase INSTANCE;
public abstract AgencyDao agencyDao();
public abstract CalendarDao calendarDao();
public abstract CalendarDateDao calendarDateDao();
public abstract FeedInfoDao feedInfoDao();
public abstract RouteDao routeDao();
public abstract StopDao stopDao();
public abstract StopTimeDao stopTimeDao();
public abstract TransferDao transferDao();
public abstract TripDao tripDao();
public static synchronized GtfsDatabase getDatabase(Context context) {
return INSTANCE == null ? INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
GtfsDatabase.class,
DATABASE_NAME
).build() : INSTANCE;
}
}
When I open the app for the first time, I fill the database with data in a background IntentService:
public static void importData(Context context, Map<String, String> data) {
GtfsDatabase db = GtfsDatabase.getDatabase(context);
db.beginTransaction();
try {
db.agencyDao().deleteAll();
db.calendarDao().deleteAll();
db.calendarDateDao().deleteAll();
db.feedInfoDao().deleteAll();
db.routeDao().deleteAll();
db.stopDao().deleteAll();
db.stopTimeDao().deleteAll();
db.transferDao().deleteAll();
db.tripDao().deleteAll();
db.agencyDao().insertAll(rawToAgencies(data.get(AGENCY_FILE)));
db.calendarDao().insertAll(rawToCalendars(data.get(CALENDAR_FILE)));
db.calendarDateDao().insertAll(rawToCalendarDates(data.get(CALENDAR_DATES_FILE)));
db.feedInfoDao().insertAll(rawToFeedInfos(data.get(FEED_INFO_FILE)));
db.routeDao().insertAll(rawToRoutes(data.get(ROUTES_FILE)));
db.tripDao().insertAll(rawToTrips(data.get(TRIPS_FILE)));
db.stopDao().insertAll(rawToStops(data.get(STOPS_FILE)));
db.stopTimeDao().insertAll(rawToStopsTimes(data.get(STOP_TIMES_FILE)));
db.transferDao().insertAll(rawToTransfers(data.get(TRANSFERS_FILE)));
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(context.getString(R.string.empty), false).apply();
} finally {
db.endTransaction();
}
}
I am absolutely sure that the data is correct. I debugged each line and I can say 100% sure that the list of objects I pass to these functions is correct. No error at all.
When this service is finished (if(!sharedPreferences.getBoolean(getString(R.string.empty), true))) I try to access to the database in another activity and this show me that is empty.
I checked with this library debugCompile 'com.amitshekhar.android:debug-db:1.0.0' and every table is really empty.
What I'm doing wrong?
I know you cannot see all my code, and maybe there's something wrong, so my actual question is: is the above code correct?
Solved.
Android Room is handling transactions automatically.
#Query are asynchronous, while #Insert and #Delete are synchronous.
My error was to try including all those operations in a single transaction.
The solution is: let that Room handles them automatically.
public static void importData(Context context, Map<String, String> data) {
GtfsDatabase db = GtfsDatabase.getDatabase(context);
db.agencyDao().deleteAll();
db.calendarDao().deleteAll();
db.calendarDateDao().deleteAll();
db.feedInfoDao().deleteAll();
db.routeDao().deleteAll();
db.stopDao().deleteAll();
db.stopTimeDao().deleteAll();
db.transferDao().deleteAll();
db.tripDao().deleteAll();
db.agencyDao().insertAll(rawToAgencies(data.get(AGENCY_FILE)));
db.calendarDao().insertAll(rawToCalendars(data.get(CALENDAR_FILE)));
db.calendarDateDao().insertAll(rawToCalendarDates(data.get(CALENDAR_DATES_FILE)));
db.feedInfoDao().insertAll(rawToFeedInfos(data.get(FEED_INFO_FILE)));
db.routeDao().insertAll(rawToRoutes(data.get(ROUTES_FILE)));
db.tripDao().insertAll(rawToTrips(data.get(TRIPS_FILE)));
db.stopDao().insertAll(rawToStops(data.get(STOPS_FILE)));
db.stopTimeDao().insertAll(rawToStopsTimes(data.get(STOP_TIMES_FILE)));
db.transferDao().insertAll(rawToTransfers(data.get(TRANSFERS_FILE)));
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(context.getString(R.string.empty), false).apply();
}

ByteBuddy subclass constructor

I am trying to create a subclass of an abstract class in bytebuddy and want to override the constructor with my own function. I can not make it work with defineConstructor.
Superclass:
public abstract class AbstractDMTable {
protected HashMap<String, DMEntry<?>> parameterMap;
public DMEntry<?> getParameter(String paramName) {
if (parameterMap.containsKey(paramName))
return parameterMap.get(paramName);
return null;
}...
Subclass:
public class DMTable_DEBUGOUT extends AbstractDMTable {
/**
* Table entry
* prints the value of the specified parameter
*/
public DMEntry<DMEntry<?>> DEBUG_PARAM;
/**
* Table entry
* execution interval of the step handler (s)
*/
public DMEntry<Double> EXEC_INTERVAL;
/**
* Table entry
* active / not active status of this subsystem
*/
public DMEntry<Boolean> IS_ACTIVE;
/**
* Standard constructor. Creates a new table and initializes all entry fields with all entry values set to {#code null}
*/
public DMTable_DEBUGOUT() {
super();
DEBUG_PARAM = new DMEntry<>();
parameterMap.put("DEBUG_PARAM", DEBUG_PARAM);
EXEC_INTERVAL = new DMEntry<>();
parameterMap.put("EXEC_INTERVAL", EXEC_INTERVAL);
IS_ACTIVE = new DMEntry<>();
parameterMap.put("IS_ACTIVE", IS_ACTIVE);
}
}
My ByteBuddy:
DynamicType.Builder<? extends AbstractDMTable> subsystem = new ByteBuddy().subclass(AbstractDMTable.class)
.name("DMTable_" + name).defineConstructor(Collections.<Class<AbstractDMTable>> emptyList(), Visibility.PUBLIC);
for (Entry<String, Pair<String, String>> p : t.getValue().entrySet()) {
subsystem.defineField(p.getKey(), this.createSubSystemEntry(p).getClass(), Visibility.PUBLIC);
}
// subsystem.defineConstructor(Arrays.<Class<AbstractDMTable>>
// asList(int.class), Visibility.PUBLIC);
return subsystem.make().load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER).getLoaded();
The Error:
defineConstructor(ModifierContributor.ForMethod...) in the type
DynamicType.Builder<AbstractDMTable> is not applicable for the
arguments (List<Class<?>>, Visibility) DynamicDatabaseGenerator.java
line 66 Java Problem
You are using the default constructor strategy which imitates the super class constructors. The subclass method is overloaded to avoid this duplicate definition by using a different constructor strategy that does not imitate the super class.
Also, you should update Byte Buddy, this way you would get a better error message.

Does PHP have an answer to Java style class generics?

Upon building an MVC framework in PHP I ran into a problem which could be solved easily using Java style generics. An abstract Controller class might look something like this:
abstract class Controller {
abstract public function addModel(Model $model);
There may be a case where a subclass of class Controller should only accept a subclass of Model. For example ExtendedController should only accept ReOrderableModel into the addModel method because it provides a reOrder() method that ExtendedController needs to have access to:
class ExtendedController extends Controller {
public function addModel(ReOrderableModel $model) {
In PHP the inherited method signature has to be exactly the same so the type hint cannot be changed to a different class, even if the class inherits the class type hinted in the superclass. In java I would simply do this:
abstract class Controller<T> {
abstract public addModel(T model);
class ExtendedController extends Controller<ReOrderableModel> {
public addModel(ReOrderableModel model) {
But there is no generics support in PHP. Is there any solution which would still adhere to OOP principles?
Edit
I am aware that PHP does not require type hinting at all but it is perhaps bad OOP. Firstly it is not obvious from the interface (the method signature) what kind of objects should be accepted. So if another developer wanted to use the method it should be obvious that objects of type X are required without them having to look through the implementation (method body) which is bad encapsulation and breaks the information hiding principle. Secondly because there's no type safety the method can accept any invalid variable which means manual type checking and exception throwing is needed all over the place!
It appears to work for me (though it does throw a Strict warning) with the following test case:
class PassMeIn
{
}
class PassMeInSubClass extends PassMeIn
{
}
class ClassProcessor
{
public function processClass (PassMeIn $class)
{
var_dump (get_class ($class));
}
}
class ClassProcessorSubClass extends ClassProcessor
{
public function processClass (PassMeInSubClass $class)
{
parent::processClass ($class);
}
}
$a = new PassMeIn;
$b = new PassMeInSubClass;
$c = new ClassProcessor;
$d = new ClassProcessorSubClass;
$c -> processClass ($a);
$c -> processClass ($b);
$d -> processClass ($b);
If the strict warning is something you really don't want, you can work around it like this.
class ClassProcessor
{
public function processClass (PassMeIn $class)
{
var_dump (get_class ($class));
}
}
class ClassProcessorSubClass extends ClassProcessor
{
public function processClass (PassMeIn $class)
{
if ($class instanceof PassMeInSubClass)
{
parent::processClass ($class);
}
else
{
throw new InvalidArgumentException;
}
}
}
$a = new PassMeIn;
$b = new PassMeInSubClass;
$c = new ClassProcessor;
$d = new ClassProcessorSubClass;
$c -> processClass ($a);
$c -> processClass ($b);
$d -> processClass ($b);
$d -> processClass ($a);
One thing you should bear in mind though, this is strictly not best practice in OOP terms. If a superclass can accept objects of a particular class as a method argument then all its subclasses should also be able of accepting objects of that class as well. Preventing subclasses from processing classes that the superclass can accept means you can't use the subclass in place of the superclass and be 100% confident that it will work in all cases. The relevant practice is known as the Liskov Substitution Principle and it states that, amongst other things, the type of method arguments can only get weaker in subclasses and the type of return values can only get stronger (input can only get more general, output can only get more specific).
It's a very frustrating issue, and I've brushed up against it plenty of times myself, so if ignoring it in a particular case is the best thing to do then I'd suggest that you ignore it. But don't make a habit of it or your code will start to develop all kinds of subtle interdependencies that will be a nightmare to debug (unit testing won't catch them because the individual units will behave as expected, it's the interaction between them where the issue lies). If you do ignore it, then comment the code to let others know about it and that it's a deliberate design choice.
Whatever the Java world invented need not be always right. I think I detected a violation of the Liskov substitution principle here, and PHP is right in complaining about it in E_STRICT mode:
Cite Wikipedia: "If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program."
T is your Controller. S is your ExtendedController. You should be able to use the ExtendedController in every place where the Controller works without breaking anything. Changing the typehint on the addModel() method breaks things, because in every place that passed an object of type Model, the typehint will now prevent passing the same object if it isn't accidentally a ReOrderableModel.
How to escape this?
Your ExtendedController can leave the typehint as is and check afterwards whether he got an instance of ReOrderableModel or not. This circumvents the PHP complaints, but it still breaks things in terms of the Liskov substitution.
A better way is to create a new method addReOrderableModel() designed to inject ReOrderableModel objects into the ExtendedController. This method can have the typehint you need, and can internally just call addModel() to put the model in place where it is expected.
If you require an ExtendedController to be used instead of a Controller as parameter, you know that your method for adding ReOrderableModel is present and can be used. You explicitly declare that the Controller will not fit in this case. Every method that expects a Controller to be passed will not expect addReOrderableModel() to exist and never attempt to call it. Every method that expects ExtendedController has the right to call this method, because it must be there.
class ExtendedController extends Controller
{
public function addReOrderableModel(ReOrderableModel $model)
{
return $this->addModel($model);
}
}
My workaround is the following:
/**
* Generic list logic and an abstract type validator method.
*/
abstract class AbstractList {
protected $elements;
public function __construct() {
$this->elements = array();
}
public function add($element) {
$this->validateType($element);
$this->elements[] = $element;
}
public function get($index) {
if ($index >= sizeof($this->elements)) {
throw new OutOfBoundsException();
}
return $this->elements[$index];
}
public function size() {
return sizeof($this->elements);
}
public function remove($element) {
validateType($element);
for ($i = 0; $i < sizeof($this->elements); $i++) {
if ($this->elements[$i] == $element) {
unset($this->elements[$i]);
}
}
}
protected abstract function validateType($element);
}
/**
* Extends the abstract list with the type-specific validation
*/
class MyTypeList extends AbstractList {
protected function validateType($element) {
if (!($element instanceof MyType)) {
throw new InvalidArgumentException("Parameter must be MyType instance");
}
}
}
/**
* Just an example class as a subject to validation.
*/
class MyType {
// blahblahblah
}
function proofOfConcept(AbstractList $lst) {
$lst->add(new MyType());
$lst->add("wrong type"); // Should throw IAE
}
proofOfConcept(new MyTypeList());
Though this still differs from Java generics, it pretty much minimalizes the extra code needed for mimicking the behaviour.
Also, it is a bit more code than some examples given by others, but - at least to me - it seems to be more clean (and more simliar to the Java counterpart) than most of them.
I hope some of you will find it useful.
Any improvements over this design are welcome!
I did went through the same type of problem before. And I used something like this to tackle it.
Class Myclass {
$objectParent = "MyMainParent"; //Define the interface or abstract class or the main parent class here
public function method($classObject) {
if(!$classObject instanceof $this -> objectParent) { //check
throw new Exception("Invalid Class Identified");
}
// Carry on with the function
}
}
You can consider to switch to Hack and HHVM. It is developed by Facebook and full compatible to PHP. You can decide to use <?php or <?hh
It support that what you want:
http://docs.hhvm.com/manual/en/hack.generics.php
I know this is not PHP. But it is compatible with it, and also improves your performance dramatically.
You can do it dirtily by passing the type as a second argument of the constructor
<?php class Collection implements IteratorAggregate{
private $type;
private $container;
public function __construct(array $collection, $type='Object'){
$this->type = $type;
foreach($collection as $value){
if(!($value instanceof $this->type)){
throw new RuntimeException('bad type for your collection');
}
}
$this->container = new \ArrayObject($collection);
}
public function getIterator(){
return $this->container->getIterator();
}
}
To provide a high level of static code-analysis, strict typing and usability, i came up with this solution: https://gist.github.com/rickhub/aa6cb712990041480b11d5624a60b53b
/**
* Class GenericCollection
*/
class GenericCollection implements \IteratorAggregate, \ArrayAccess{
/**
* #var string
*/
private $type;
/**
* #var array
*/
private $items = [];
/**
* GenericCollection constructor.
*
* #param string $type
*/
public function __construct(string $type){
$this->type = $type;
}
/**
* #param $item
*
* #return bool
*/
protected function checkType($item): bool{
$type = $this->getType();
return $item instanceof $type;
}
/**
* #return string
*/
public function getType(): string{
return $this->type;
}
/**
* #param string $type
*
* #return bool
*/
public function isType(string $type): bool{
return $this->type === $type;
}
#region IteratorAggregate
/**
* #return \Traversable|$type
*/
public function getIterator(): \Traversable{
return new \ArrayIterator($this->items);
}
#endregion
#region ArrayAccess
/**
* #param mixed $offset
*
* #return bool
*/
public function offsetExists($offset){
return isset($this->items[$offset]);
}
/**
* #param mixed $offset
*
* #return mixed|null
*/
public function offsetGet($offset){
return isset($this->items[$offset]) ? $this->items[$offset] : null;
}
/**
* #param mixed $offset
* #param mixed $item
*/
public function offsetSet($offset, $item){
if(!$this->checkType($item)){
throw new \InvalidArgumentException('invalid type');
}
$offset !== null ? $this->items[$offset] = $item : $this->items[] = $item;
}
/**
* #param mixed $offset
*/
public function offsetUnset($offset){
unset($this->items[$offset]);
}
#endregion
}
/**
* Class Item
*/
class Item{
/**
* #var int
*/
public $id = null;
/**
* #var string
*/
public $data = null;
/**
* Item constructor.
*
* #param int $id
* #param string $data
*/
public function __construct(int $id, string $data){
$this->id = $id;
$this->data = $data;
}
}
/**
* Class ItemCollection
*/
class ItemCollection extends GenericCollection{
/**
* ItemCollection constructor.
*/
public function __construct(){
parent::__construct(Item::class);
}
/**
* #return \Traversable|Item[]
*/
public function getIterator(): \Traversable{
return parent::getIterator();
}
}
/**
* Class ExampleService
*/
class ExampleService{
/**
* #var ItemCollection
*/
private $items = null;
/**
* SomeService constructor.
*
* #param ItemCollection $items
*/
public function __construct(ItemCollection $items){
$this->items = $items;
}
/**
* #return void
*/
public function list(){
foreach($this->items as $item){
echo $item->data;
}
}
}
/**
* Usage
*/
$collection = new ItemCollection;
$collection[] = new Item(1, 'foo');
$collection[] = new Item(2, 'bar');
$collection[] = new Item(3, 'foobar');
$collection[] = 42; // InvalidArgumentException: invalid type
$service = new ExampleService($collection);
$service->list();
Even if something like this would feel so much better:
class ExampleService{
public function __construct(Collection<Item> $items){
// ..
}
}
Hope generics will get into PHP soon.
One alternative is the combination of splat operator + typed hint + private array:
<?php
class Student {
public string $name;
public function __construct(string $name){
$this->name = $name;
}
}
class Classe {
private $students = [];
public function add(Student ...$student){
array_merge($this->students, $student);
}
public function getAll(){
return $this->students;
}
}
$c = new Classe();
$c->add(new Student('John'), new Student('Mary'), new Student('Kate'));

Categories