diff --git a/.gitignore b/.gitignore index 28d2140..8c3922b 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /phpunit.xml /composer.lock -/vendor \ No newline at end of file +/vendor +.env \ No newline at end of file diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..f3b209b --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"Parziphal\\Parse\\Test\\ModelTest::testPersistance":4,"Parziphal\\Parse\\Test\\ModelTest::testBelongsToMany":4,"Parziphal\\Parse\\Test\\ModelTest::testHasManyArray":4,"Parziphal\\Parse\\Test\\ModelTest::testBelongsToAndHasMany":4,"Parziphal\\Parse\\Test\\ModelTest::testHasMany":4,"Parziphal\\Parse\\Test\\ModelTest::testLogin":4,"Parziphal\\Parse\\Test\\ModelTest::testPagination":3,"Parziphal\\Parse\\Test\\ModelTest::testPaginateWithWith":3,"Parziphal\\Parse\\Test\\ModelTest::testSimplePagination":4,"Parziphal\\Parse\\Test\\ModelTest::testWhenMethod":4},"times":{"Parziphal\\Parse\\Test\\ModelTest::testPersistance":0.565,"Parziphal\\Parse\\Test\\ModelTest::testBelongsToMany":0.526,"Parziphal\\Parse\\Test\\ModelTest::testHasManyArray":0.885,"Parziphal\\Parse\\Test\\ModelTest::testBelongsToAndHasMany":0.354,"Parziphal\\Parse\\Test\\ModelTest::testHasMany":0.694,"Parziphal\\Parse\\Test\\ModelTest::testLogin":0.07,"Parziphal\\Parse\\Test\\ModelTest::testPagination":2.995,"Parziphal\\Parse\\Test\\ModelTest::testSimplePagination":1.928,"Parziphal\\Parse\\Test\\ModelTest::testWhenMethod":0.459,"Parziphal\\Parse\\Test\\ModelTest::testPaginateWithWith":2.16}} \ No newline at end of file diff --git a/composer.json b/composer.json index a73fe52..525c20a 100755 --- a/composer.json +++ b/composer.json @@ -4,9 +4,9 @@ "keywords": ["laravel", "parse", "sdk", "parse-sdk", "eloquent"], "license": "MIT", "require": { - "php": ">=5.6", - "laravel/framework": "^5.2 || ^6.0 || ^7.0", - "parse/php-sdk": "^1.2.0" + "php": ">=8.1", + "laravel/framework": "^9.0 || ^10.0 || ^11.0", + "parse/php-sdk": "^2.4" }, "autoload": { "psr-4": { @@ -21,6 +21,13 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.1" + "phpunit/phpunit": "^9.1", + "squizlabs/php_codesniffer": "*", + "phpcompatibility/php-compatibility": "*" + }, + "config": { + "allow-plugins": { + "kylekatarnls/update-helper": true + } } } diff --git a/src/Auth/AuthenticatesWithFacebook.php b/src/Auth/AuthenticatesWithFacebook.php deleted file mode 100755 index 9fbfc9d..0000000 --- a/src/Auth/AuthenticatesWithFacebook.php +++ /dev/null @@ -1,115 +0,0 @@ - true]; - - public function logInOrRegisterWithFacebookApi(Request $request) - { - $this->logInOrRegisterWithFacebook($request); - - return response()->json($this->apiResponse); - } - - public function logInOrRegisterWithFacebookRedirect(Request $request) - { - $this->logInOrRegisterWithFacebook($request); - - return redirect($this->redirectPath()); - } - - /** - * Accepts both username|email/password and Facebook registration requests. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - public function registerAny(Request $request) - { - if ($request->id && $request->auth_token) { - return $this->registerWithFacebook($request); - } else { - return $this->register($request); - } - } - - /** - * Registers a new user with Facebook, but the user isn't logged in to Laravel. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - public function registerWithFacebookRedirect(Request $request) - { - $this->logInWithFacebook($request); - - return redirect($this->redirectPath()); - } - - /** - * Registers a new user with Facebook, but the user isn't logged in to Laravel. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - public function registerWithFacebookApi(Request $request) - { - $this->logInWithFacebook($request); - - return response()->json($this->apiResponse); - } - - /** - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - public function logoutApi(Request $request) - { - $this->logout($request); - - return response()->json($this->apiResponse); - } - - /** - * Registers a new user and/or logs the user in to Laravel. - * - * @param \Illuminate\Http\Request $request - * @return void - */ - protected function logInOrRegisterWithFacebook(Request $request) - { - $user = $this->logInWithFacebook($request); - - if (method_exists($this, 'getGuard')) { - // Laravel 5.2 - Auth::guard($this->getGuard())->login($user, $request->has('remember')); - } else { - // Laravel 5.3+ - $this->guard()->login($user, $request->has('remember')); - } - } - - /** - * Registers a new user or log in into Parse if the user exists. - * Returns null if an error occured. - * - * @param Request $request - * @return \Parziphal\Parse\UserModel|null - */ - protected function logInWithFacebook(Request $request) - { - $class = config('auth.providers.users.model'); - - return $class::logInWithFacebook($request->id, $request->access_token); - } -} diff --git a/src/Auth/Providers/AnyUserProvider.php b/src/Auth/Providers/AnyUserProvider.php deleted file mode 100755 index 3edb4d9..0000000 --- a/src/Auth/Providers/AnyUserProvider.php +++ /dev/null @@ -1,40 +0,0 @@ -isFacebookLogIn($credentials)) { - return $this->retrieveByFacebook($credentials); - } else { - return $this->retrieveByUsername($credentials); - } - } - - /** - * Validate a user against the given credentials. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param array $credentials - * @return bool - */ - public function validateCredentials(Authenticatable $user, array $credentials) - { - if ($this->isFacebookLogIn($credentials)) { - return $this->validateWithFacebook($user, $credentials); - } else { - return $this->validateWithPassword($user, $credentials); - } - } -} diff --git a/src/Auth/Providers/BaseProvider.php b/src/Auth/Providers/BaseProvider.php deleted file mode 100755 index 7225450..0000000 --- a/src/Auth/Providers/BaseProvider.php +++ /dev/null @@ -1,138 +0,0 @@ -userClass = $userClass; - } - - /** - * Retrieve a user by their unique identifier. - * - * @param mixed $identifier - * @return \Illuminate\Contracts\Auth\Authenticatable|null - */ - public function retrieveById($identifier) - { - $class = $this->userClass; - - return $class::query(true)->find($identifier) ?: null; - } - - /** - * Retrieve a user by their unique identifier and "remember me" token. - * - * @param mixed $identifier - * @param string $token - * @return \Illuminate\Contracts\Auth\Authenticatable|null - */ - public function retrieveByToken($identifier, $token) - { - $class = $this->userClass; - - return $class::query(true)->where([ - 'objectId' => $identifier, - 'rememberToken' => $token - ])->first(); - } - - /** - * Update the "remember me" token for the given user in storage. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param string $token - * @return void - */ - public function updateRememberToken(Authenticatable $user, $token) - { - $user->update(['rememberToken' => $token], true); - } - - protected function validatePassword(Authenticatable $user, array $credentials) - { - $username = $this->getUsernameFromCredentials($credentials); - $userClass = get_class($user); - - try { - $userClass::logIn($username, $credentials['password']); - } catch (ParseException $e) { - return false; - } - - return true; - } - - protected function getUsernameFromCredentials(array $credentials) - { - $username = null; - - if (empty($credentials['username'])) { - if (!empty($credentials['email'])) { - $username = $credentials['email']; - } - } else { - $username = $credentials['username']; - } - - return $username; - } - - protected function isFacebookLogIn(array $credentials) - { - return array_key_exists('access_token', $credentials) && array_key_exists('id', $credentials); - } - - protected function retrieveByUsername(array $credentials) - { - $class = $this->userClass; - - $username = $this->getUsernameFromCredentials($credentials); - - return $class::query(true)->where(['username' => $username])->first(); - } - - protected function retrieveByFacebook(array $credentials) - { - $class = $this->userClass; - - // Check if the user exists first. If we call logInWithFacebook right away, - // the user would be created. - $user = $class::query(true)->where(['authData.facebook.id' => $credentials['id']])->first(); - - if (!$user) { - return; - } - - try { - return $class::logInWithFacebook($credentials['id'], $credentials['access_token']); - } catch (ParseException $e) { - return; - } - } - - protected function validateWithPassword(Authenticatable $user, array $credentials) - { - return $this->validatePassword($user, $credentials); - } - - protected function validateWithFacebook(Authenticatable $user, array $credentials) - { - /** - * If we got here, it means that retrieveByFacebook() returned successfully - * in retrieveByCredentials(). - */ - return true; - } -} diff --git a/src/Auth/Providers/FacebookUserProvider.php b/src/Auth/Providers/FacebookUserProvider.php deleted file mode 100755 index e449ff2..0000000 --- a/src/Auth/Providers/FacebookUserProvider.php +++ /dev/null @@ -1,32 +0,0 @@ -retrieveByFacebook($credentials); - } - - /** - * Validate a user against the given credentials. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param array $credentials - * @return bool - */ - public function validateCredentials(Authenticatable $user, array $credentials) - { - return $this->validateWithFacebook($user, $credentials); - } -} diff --git a/src/Auth/Providers/UserProvider.php b/src/Auth/Providers/UserProvider.php deleted file mode 100755 index c4743f5..0000000 --- a/src/Auth/Providers/UserProvider.php +++ /dev/null @@ -1,32 +0,0 @@ -retrieveByUsername($credentials); - } - - /** - * Validate a user against the given credentials. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param array $credentials - * @return bool - */ - public function validateCredentials(Authenticatable $user, array $credentials) - { - return $this->validateWithPassword($user, $credentials); - } -} diff --git a/src/Auth/Recaller.php b/src/Auth/Recaller.php deleted file mode 100755 index 9ea473a..0000000 --- a/src/Auth/Recaller.php +++ /dev/null @@ -1,84 +0,0 @@ -recaller = $recaller; - } - - /** - * Get the user ID from the recaller. - * - * @return string - */ - public function id() - { - return explode('|', $this->recaller, 3)[0]; - } - - /** - * Get the "remember token" token from the recaller. - * - * @return string - */ - public function token() - { - return explode('|', $this->recaller, 3)[1]; - } - - public function sessionId() - { - return explode('|', $this->recaller, 3)[2]; - } - - /** - * Determine if the recaller is valid. - * - * @return bool - */ - public function valid() - { - return $this->properString() && $this->hasAllSegments(); - } - - /** - * Determine if the recaller is an invalid string. - * - * @return bool - */ - protected function properString() - { - return is_string($this->recaller) && mb_strpos($this->recaller, '|') !== false; - } - - /** - * Determine if the recaller has all segments. - * - * @return bool - */ - protected function hasAllSegments() - { - $segments = explode('|', $this->recaller); - - return count($segments) == 3 && trim($segments[0]) !== '' && trim($segments[1]) !== '' && trim($segments[2]) !== ''; - } -} \ No newline at end of file diff --git a/src/Auth/SessionGuard.php b/src/Auth/SessionGuard.php deleted file mode 100755 index 931d02b..0000000 --- a/src/Auth/SessionGuard.php +++ /dev/null @@ -1,78 +0,0 @@ -recaller->sessionId(); - - if ($sessionToken) { - try { - ParseUser::become($sessionToken); - ParseClient::getStorage()->set('user', ParseUser::getCurrentUser()); - } catch (ParseException $e) { - } - } - - if (!ParseUser::getCurrentUser()) { - // Laravel knows the user but Parse doesn't and we don't have - // the session token to login the user to Parse. - return null; - } - } - - return $user; - } - - public function logout() - { - parent::logout(); - ParseUser::logOut(); - } - - protected function recaller() - { - if (is_null($this->request)) { - return; - } elseif ($this->recaller) { - return $this->recaller; - } - - if ($recaller = $this->request->cookies->get($this->getRecallerName())) { - $this->recaller = new Recaller($recaller); - } - - return $this->recaller; - } - - protected function queueRecallerCookie(AuthenticatableContract $user) - { - $this->getCookieJar()->queue($this->createRecaller( - $user->getAuthIdentifier().'|'.$user->getRememberToken().'|'.ParseSession::getCurrentSession()->getSessionToken() - )); - } -} \ No newline at end of file diff --git a/src/Auth/UserModel.php b/src/Auth/UserModel.php deleted file mode 100755 index 970bf78..0000000 --- a/src/Auth/UserModel.php +++ /dev/null @@ -1,36 +0,0 @@ -id(); - } - - public function __construct($data = null, $useMasterKey = null) - { - parent::__construct($data, $useMasterKey); - - $this->rememberTokenName = 'rememberToken'; - } -} diff --git a/src/Console/ModelMakeCommand.php b/src/Console/ModelMakeCommand.php index f90ed46..d599ab5 100755 --- a/src/Console/ModelMakeCommand.php +++ b/src/Console/ModelMakeCommand.php @@ -28,6 +28,17 @@ class ModelMakeCommand extends GeneratorCommand */ protected $type = 'ObjectModel'; + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace . '\\Models\\Parse'; + } + /** * Get the stub file for the generator. * diff --git a/src/ObjectModel.php b/src/ObjectModel.php index 2c3dd22..9a4f45e 100755 --- a/src/ObjectModel.php +++ b/src/ObjectModel.php @@ -4,15 +4,13 @@ use Closure; use DateTime; -use Traversable; +use Illuminate\Database\Eloquent\Collection; use LogicException; -use Parse\ParseACL; use ReflectionClass; use Parse\ParseFile; use JsonSerializable; use Parse\ParseObject; use Illuminate\Support\Arr; -use Parse\Internal\Encodable; use Illuminate\Support\Pluralizer; use Parziphal\Parse\Relations\HasOne; use Parziphal\Parse\Relations\HasMany; @@ -23,6 +21,26 @@ use Parziphal\Parse\Relations\HasManyArray; use Parziphal\Parse\Relations\BelongsToMany; +/** + * @method static Query where($column, $operator = null, $value = null) + * @method static ObjectModel|null find(string $objectId, $selectKeys = null) + * @method static Collection get(string|string[] $selectKeys = null) + * @method static ObjectModel|null first($selectKeys = null) + * @method static ObjectModel firstOrFail($selectKeys = null) + * @method static int count() + * @method static Query orderBy($key, $order = 1) + * @method static Query whereIn($key, $values) + * @method static ObjectModel findOrFail(string $objectId, $selectKeys = null) + * @method static object|ObjectModel findOrNew(string $objectId, $selectKeys = null) + * @method static ObjectModel firstOrNew(array $data) + * @method static ObjectModel firstOrCreate(array $data) + * @method static Query matchesQuery(string $key, Query|ParseQuery $query) + * @method static Query doesNotMatchQuery(string $key, Query|ParseQuery $query) + * @method static Query matchesKeyInQuery(string $key, string $queryKey, Query|ParseQuery $query) + * @method static Query doesNotMatchKeyInQuery(string $key, string $queryKey, Query|ParseQuery $query) + * @method static Query containedIn(string $key, array $values) + * @method static Query with(array|string $keys) + */ abstract class ObjectModel implements Arrayable, Jsonable, JsonSerializable { protected static $parseClassName; @@ -473,7 +491,7 @@ public function toJson($options = 0) return json_encode($this->jsonSerialize(), $options); } - public function jsonSerialize() + public function jsonSerialize(): mixed { return $this->toArray(); } diff --git a/src/ParseServiceProvider.php b/src/ParseServiceProvider.php index ada514d..933521c 100755 --- a/src/ParseServiceProvider.php +++ b/src/ParseServiceProvider.php @@ -4,16 +4,15 @@ use Parse\ParseClient; use Parziphal\Parse\SessionStorage; -use Illuminate\Support\Facades\Auth; -use Parziphal\Parse\ParseUserProvider; +// use Illuminate\Support\Facades\Auth; use Illuminate\Support\ServiceProvider; use Parziphal\Parse\Console\ModelMakeCommand; -use Parziphal\Parse\Auth\Providers\UserProvider; -use Laravel\Lumen\Application as LumenApplication; -use Parziphal\Parse\Auth\Providers\AnyUserProvider; -use Parziphal\Parse\Auth\Providers\FacebookUserProvider; +// use Parziphal\Parse\Auth\Providers\UserProvider; +// use Laravel\Lumen\Application as LumenApplication; +// use Parziphal\Parse\Auth\Providers\AnyUserProvider; +// use Parziphal\Parse\Auth\Providers\FacebookUserProvider; use Illuminate\Foundation\Application as LaravelApplication; -use Parziphal\Parse\Auth\SessionGuard; +// use Parziphal\Parse\Auth\SessionGuard; class ParseServiceProvider extends ServiceProvider { @@ -42,9 +41,10 @@ protected function setupConfig() if ($this->app instanceof LaravelApplication && $this->app->runningInConsole()) { $this->publishes([$source => config_path('parse.php')], 'parse'); - } elseif ($this->app instanceof LumenApplication) { - $this->app->configure('parse'); } + // elseif ($this->app instanceof LumenApplication) { + // $this->app->configure('parse'); + // } $this->mergeConfigFrom($source, 'parse'); } @@ -77,28 +77,28 @@ protected function setupParse() ParseClient::setServerURL($config['server_url'], $config['mount_path']); // Register providers - Auth::provider('parse', function($app, array $config) { - return new UserProvider($config['model']); - }); + // Auth::provider('parse', function($app, array $config) { + // return new UserProvider($config['model']); + // }); - Auth::provider('parse-facebook', function($app, array $config) { - return new FacebookUserProvider($config['model']); - }); + // Auth::provider('parse-facebook', function($app, array $config) { + // return new FacebookUserProvider($config['model']); + // }); - Auth::provider('parse-any', function($app, array $config) { - return new AnyUserProvider($config['model']); - }); + // Auth::provider('parse-any', function($app, array $config) { + // return new AnyUserProvider($config['model']); + // }); - // Register guard - Auth::extend('session-parse', function($app, $name, array $config) { - $guard = new SessionGuard($name, Auth::createUserProvider($config['provider']), $app['session.store']); + // // Register guard + // Auth::extend('session-parse', function($app, $name, array $config) { + // $guard = new SessionGuard($name, Auth::createUserProvider($config['provider']), $app['session.store']); - $guard->setCookieJar($this->app['cookie']); - $guard->setDispatcher($this->app['events']); - $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); + // $guard->setCookieJar($this->app['cookie']); + // $guard->setDispatcher($this->app['events']); + // $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); - return $guard; - }); + // return $guard; + // }); } /** diff --git a/src/Query.php b/src/Query.php index d6cfd1c..ba18b26 100755 --- a/src/Query.php +++ b/src/Query.php @@ -8,6 +8,8 @@ use Parse\ParseObject; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Pagination\Paginator; class Query { @@ -282,7 +284,8 @@ public function findOrNew($objectId, $selectKeys = null) */ public function first($selectKeys = null) { - if ($selectKeys) { + // Treat '*' or ['*'] as a wildcard (no-op) to avoid passing it to Parse SDK + if ($selectKeys !== null && $selectKeys !== '*' && $selectKeys !== ['*']) { $this->parseQuery->select($selectKeys); } @@ -368,7 +371,8 @@ public function firstOrCreate(array $data) */ public function get($selectKeys = null) { - if ($selectKeys) { + // Treat '*' or ['*'] as a wildcard (no-op) to avoid passing it to Parse SDK + if ($selectKeys !== null && $selectKeys !== '*' && $selectKeys !== ['*']) { $this->select($selectKeys); } @@ -437,10 +441,31 @@ public function doesNotMatchKeyInQuery($key, $queryKey, $query) public function orderBy($key, $order = 1) { - if ($order == 1) { - $this->ascending($key); - } else { - $this->descending($key); + $check = is_string($order) ? strtolower($order) : $order; + + match ($check) { + 1, 'asc', 'ascending' => $this->parseQuery->ascending($key), + 0, 'desc', 'descending' => $this->parseQuery->descending($key), + default => null, // Hoặc log lỗi nếu muốn + }; + + return $this; + } + + /** + * Apply the given callback if the value is truthy. + * + * @param mixed $value + * @param \Closure $callback + * @param \Closure|null $default + * @return $this + */ + public function when($value, Closure $callback, ?Closure $default = null) + { + if ($value) { + $callback($this, $value); + } elseif ($default) { + $default($this, $value); } return $this; @@ -484,15 +509,23 @@ public function count() * * @return $this */ - public function with($keys) + public function with(...$keys) { - if (is_string($keys)) { - $keys = func_get_args(); + // Allow passing a single array or multiple string arguments + if (count($keys) === 1 && is_array($keys[0])) { + $keys = $keys[0]; } $this->includeKeys = array_merge($this->includeKeys, $keys); - $this->parseQuery->includeKey($keys); + // ParseQuery::includeKey expects a string; call once per key + if (is_array($keys)) { + foreach ($keys as $k) { + $this->parseQuery->includeKey($k); + } + } else { + $this->parseQuery->includeKey($keys); + } return $this; } @@ -505,6 +538,167 @@ public function getParseQuery() return $this->parseQuery; } + /** + * Paginate the query results with a length-aware paginator. + * + * @param int $perPage + * @param mixed $selectKeys + * @param string $pageName + * @param int|null $page + * @return LengthAwarePaginator + */ + public function paginate($perPage = 15, $selectKeys = null, $pageName = 'page', $page = null) + { + $page = $page ?: Paginator::resolveCurrentPage($pageName); + + // Clone parseQuery for counting so we don't affect the main query + $countQuery = clone $this->parseQuery; + + $total = $countQuery->count($this->useMasterKey); + + $itemsQuery = $this->parseQuery; + $itemsQuery->limit($perPage); + $itemsQuery->skip(($page - 1) * $perPage); + + // Treat '*' or ['*'] as a wildcard (no-op) to avoid passing it to Parse SDK + if ($selectKeys !== null && $selectKeys !== '*' && $selectKeys !== ['*']) { + $itemsQuery->select($selectKeys); + } + + $objects = $itemsQuery->find($this->useMasterKey); + + $items = $this->createModels($objects); + + $paginator = new LengthAwarePaginator($items, $total, $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); + + return $paginator; + } + + /** + * Simple pagination without total count. + * + * @param int $perPage + * @param mixed $selectKeys + * @param string $pageName + * @param int|null $page + * @return Paginator + */ + public function simplePaginate($perPage = 15, $selectKeys = null, $pageName = 'page', $page = null) + { + $page = $page ?: Paginator::resolveCurrentPage($pageName); + + $itemsQuery = clone $this->parseQuery; + $itemsQuery->limit($perPage); + $itemsQuery->skip(($page - 1) * $perPage); + + // Treat '*' or ['*'] as a wildcard (no-op) to avoid passing it to Parse SDK + if ($selectKeys !== null && $selectKeys !== '*' && $selectKeys !== ['*']) { + $itemsQuery->select($selectKeys); + } + + $objects = $itemsQuery->find($this->useMasterKey); + + $items = $this->createModels($objects); + + $paginator = new Paginator($items, $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ]); + + return $paginator; + } + + /** + * Chunk the results by primary key to avoid loading too much data into memory. + * + * @param int $count + * @param \Closure $callback + * @param string $column + * @param string|null $alias + * @return bool + */ + public function chunkById($count, Closure $callback, $column = 'objectId', $alias = null) + { + $lastId = null; + + do { + $query = clone $this->parseQuery; + + // apply ordering by the id column + $query->ascending($column); + + if ($lastId) { + $query->greaterThan($column, $lastId); + } + + // If an alias is provided, ensure the underlying column is selected + // so we can read its value even if the developer used a select earlier. + if ($alias !== null && $alias !== $column) { + try { + $query->select([$column]); + } catch (\Throwable $e) { + // ignore: Parse SDK may throw if select is incompatible; rely on existing selection + } + } + + $query->limit($count); + + $objects = $query->find($this->useMasterKey); + + $models = $this->createModels($objects); + + if ($models->isEmpty()) { + break; + } + + // Call the callback with the current chunk. If it returns false, stop. + $continue = $callback($models); + + if ($continue === false) { + return false; + } + + // Set lastId to the last model's id (respecting alias) for next iteration + $last = $models->last(); + + $lastId = null; + + if ($alias !== null) { + // Try reading the alias attribute from the model + if (is_object($last) && (property_exists($last, $alias) || method_exists($last, '__get') || method_exists($last, $alias))) { + try { + $val = $last->{$alias}; + if ($val !== null) { + $lastId = $val; + } + } catch (\Throwable $e) { + // ignore and fallback + } + } + } + + if ($lastId === null) { + // Fallbacks: Prefer object id when ordering by objectId + if ($column === 'objectId' && is_object($last) && method_exists($last, 'id')) { + $lastId = $last->id(); + } else { + // try reading the column as attribute + try { + $lastId = is_object($last) ? $last->{$column} : null; + } catch (\Throwable $e) { + $lastId = null; + } + } + } + + } while ($models->count() == $count); + + return true; + } + protected function createModel(ParseObject $data) { $className = $this->fullClassName; diff --git a/src/SessionStorage.php b/src/SessionStorage.php index bf6fb5e..a5d8c4d 100755 --- a/src/SessionStorage.php +++ b/src/SessionStorage.php @@ -51,7 +51,7 @@ public function get($key) */ public function clear() { - Sessions::clear(); + Session::clear(); } /** diff --git a/src/UserModel.php b/src/UserModel.php deleted file mode 100755 index 16cb701..0000000 --- a/src/UserModel.php +++ /dev/null @@ -1,83 +0,0 @@ -signUp(); - - return $model; - } - - /** - * @param ParseUser|array $data - * @param bool $useMasterKey - */ - public function __construct($data = null, $useMasterKey = null) - { - if ($data != null && !$data instanceof ParseUser && !is_array($data)) { - $type = is_object($data) ? get_class($data) : gettype($data); - - throw new Exception( - sprintf("Either a ParseUser or an array must be passed to instantiate a UserModel, %s passed", $type) - ); - } - - if ($data instanceof ParseUser) { - $this->parseObject = $data; - } else { - $this->parseObject = new ParseUser(static::parseClassName()); - - if ($data) { - $this->fill($data); - } - } - - $this->useMasterKey = $useMasterKey !== null ? $useMasterKey : static::$defaultUseMasterKey; - } - - public function linkWithFacebook($id, $accessToken, $expirationDate = null, $useMasterKey = false) - { - return new static($this->parseObject->loginWithAnonymous( - $id, - $accessToken, - $expirationDate, - $useMasterKey - )); - } -} diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 01494a6..fc1a9bb 100755 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -3,12 +3,14 @@ namespace Parziphal\Parse\Test; use Parse\ParseObject; +use Parse\ParseUser; use PHPUnit\Framework\TestCase; use Parziphal\Parse\ObjectModel; use Parziphal\Parse\Test\Models\Post; use Parziphal\Parse\Test\Models\User; use Parziphal\Parse\Test\Models\Category; use Illuminate\Database\Eloquent\ModelNotFoundException; +use PHPUnit\Framework\Assert; class ModelTest extends TestCase { @@ -135,4 +137,79 @@ public function testHasMany() $this->assertSame(2, $user->posts->count()); } + + public function testPagination() + { + // Clean up any existing posts if necessary - depends on test environment + + // Create 35 posts + $initialTotal = Post::query()->count(); + + for ($i = 1; $i <= 35; $i++) { + Post::create(['title' => 'Post ' . $i]); + } + + // Paginate with 10 per page + $p = Post::query()->with('categories', 'user')->paginate(10, ['*'], 'page'); + Assert::fail(print_r($p, true)); + + $this->assertInstanceOf(\Illuminate\Pagination\LengthAwarePaginator::class, $p); + $this->assertSame(10, $p->perPage()); + $this->assertSame($initialTotal + 35, $p->total()); + $this->assertSame((int) ceil(($initialTotal + 35) / 10), $p->lastPage()); + $this->assertCount(10, $p->items()); + } + + public function testSimplePagination() + { + $initialTotal = Post::query()->count(); + + // Create 25 posts + for ($i = 1; $i <= 25; $i++) { + Post::create(['title' => 'Simple Post ' . $i]); + } + + // Page 1 + $p1 = Post::query()->simplePaginate(10, null, 'page', 1); + + $this->assertInstanceOf(\Illuminate\Pagination\Paginator::class, $p1); + $this->assertSame(10, $p1->perPage()); + $this->assertCount(10, $p1->items()); + $this->assertTrue(method_exists($p1, 'hasMorePages')); + + // Page 3 + $p3 = Post::query()->simplePaginate(10, null, 'page', 3); + + $this->assertCount(10, $p3->items()); + } + + public function testWhenMethod() + { + // ensure some posts exist + for ($i = 1; $i <= 5; $i++) { + Post::create(['title' => 'When Post ' . $i]); + } + + $baseQuery = Post::query(); + + // apply when true - filter by title containing 'When Post' + $q1 = $baseQuery->when(true, function ($q) { + $q->where('title', 'When Post 1'); + }); + + $result1 = $q1->get(); + + $this->assertIsIterable($result1); + + // apply when false - default closure should run + $q2 = Post::query()->when(false, function ($q) { + $q->where('title', 'Should Not Match'); + }, function ($q) { + // default - do nothing + }); + + $result2 = $q2->get(); + + $this->assertIsIterable($result2); + } } diff --git a/tests/boot.php b/tests/boot.php index 4e1738f..9c3359f 100755 --- a/tests/boot.php +++ b/tests/boot.php @@ -9,8 +9,10 @@ $appId = getenv('parseAppId'); $masterKey = getenv('parseMasterKey'); + +$restKey = getenv('parseRestKey'); $serverUrl = getenv('parseServerUrl'); $mountPath = getenv('parseMountPath'); -ParseClient::initialize($appId, null, $masterKey); +ParseClient::initialize($appId, $restKey, $masterKey); ParseClient::setServerURL($serverUrl, $mountPath);