Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions compiler/cpp/src/thrift/generate/t_php_generator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1941,7 +1941,7 @@ void t_php_generator::generate_service_rest(t_service* tservice) {
<< (*a_iter)->get_name() << ", true);" << '\n';
} else if (atype->is_set()) {
f_service_rest << indent() << "$" << (*a_iter)->get_name() << " = array_fill_keys(json_decode($"
<< (*a_iter)->get_name() << ", true), 1);" << '\n';
<< (*a_iter)->get_name() << ", true), true);" << '\n';
} else if (atype->is_struct() || atype->is_xception()) {
f_service_rest << indent() << "if ($" << (*a_iter)->get_name() << " !== null) {" << '\n'
<< indent() << " $" << (*a_iter)->get_name() << " = new "
Expand Down Expand Up @@ -2682,13 +2682,33 @@ void t_php_generator::generate_serialize_container(ostream& out, t_type* ttype,
} else if (ttype->is_set()) {
string iter = tmp("iter");
string iter_val = tmp("iter");
indent(out) << "foreach ($" << prefix << " as $" << iter << " => $" << iter_val << ") {" << '\n';
indent_up();

t_type* elem_type = ((t_set*)ttype)->get_elem_type();
if(php_is_scalar(elem_type)) {
generate_serialize_set_element(out, (t_set*)ttype, iter);
if (php_is_scalar(elem_type)) {
string set_uses_values = tmp("setUsesValues");
// Preserve the legacy `element => true` marker form when every value is
// `true`. This keeps ambiguous `set<bool>` inputs such as `[true]` on
// the backward-compatible path instead of guessing they are value lists.
string list_val = tmp("iter");
string iter_elem = tmp("iter");
indent(out) << "$" << set_uses_values << " = false;" << '\n';
indent(out) << "if (array_is_list($" << prefix << ")) {" << '\n';
indent_up();
indent(out) << "foreach ($" << prefix << " as $" << list_val << ") {" << '\n';
indent_up();
indent(out) << "if ($" << list_val << " !== true) {" << '\n';
indent_up();
indent(out) << "$" << set_uses_values << " = true;" << '\n';
indent(out) << "break;" << '\n';
scope_down(out);
scope_down(out);
scope_down(out);
indent(out) << "foreach ($" << prefix << " as $" << iter << " => $" << iter_val << ") {" << '\n';
indent_up();
indent(out) << "$" << iter_elem << " = $" << set_uses_values << " ? $" << iter_val << " : $" << iter << ";" << '\n';
generate_serialize_set_element(out, (t_set*)ttype, iter_elem);
Comment thread
sveneld marked this conversation as resolved.
} else {
indent(out) << "foreach ($" << prefix << " as $" << iter << " => $" << iter_val << ") {" << '\n';
indent_up();
generate_serialize_set_element(out, (t_set*)ttype, iter_val);
}
scope_down(out);
Expand Down Expand Up @@ -2755,9 +2775,10 @@ void t_php_generator::generate_serialize_map_element(ostream& out,
* Serializes the members of a set.
*/
void t_php_generator::generate_serialize_set_element(ostream& out, t_set* tset, string iter) {
// Set element used as PHP array key — same coercion concern as map keys;
// see comment on emit_array_key_recast. Helper no-ops for non-castable
// element types.
// Scalar PHP sets may be represented either as a legacy keyed array of
// element => true markers or as a plain list of element values. Cast when
// needed so typed writeXxx() calls accept scalars from either form, including
// keys coerced by PHP and value-list elements convertible to the declared type.
emit_array_key_recast(out, tset->get_elem_type(), iter);

t_field efield(tset->get_elem_type(), iter);
Expand Down
15 changes: 15 additions & 0 deletions lib/php/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ apcu_fetch(), apcu_store()

1. The legacy callable/string `$debugHandler` argument has been removed from `TSocket`, `TSSLSocket` and `TSocketPool`. The constructors now accept a PSR-3 logger via the `$logger` parameter instead. `TSocket::setDebug()`, `TSocket::DEFAULT_DEBUG_HANDLER` and `TSSLServerSocket::getSSLHost()` have also been removed. The `ssl://` prefix is still applied automatically by `TSSLServerSocket` and `TSSLSocket`. Additionally, `TSocket::open()` no longer falls back to the send timeout for the connect step; use `TSocket::setConnectTimeout()` to configure a dedicated connect timeout. The default connect timeout is now 1 second.

### Scalar sets

Scalar sets now also accept sequential lists of values, such as `[10, 20]`.
Keyed sets should use boolean `true` markers: `array_fill_keys($elements, true)`.
Previously the marker value was ignored. A keyed set whose keys are `0..n-1`
and whose markers are not all strictly `true` is now interpreted as a value list;
for example, `array_fill_keys([0, 1], 1)` is interpreted as the values `[1, 1]`.
Update callers using other markers to use `true`. Generated REST wrappers and
deserialized sets use `true` markers.

An all-`true` list retains the keyed interpretation for compatibility: `[true]`
represents the key `0`, so use `[1 => true]` to represent a `set<bool>` containing
only `true`. Scalar set elements are cast to their declared Thrift type in both
forms, consistently across generated serializers and the OOP runtime.

## 0.12.0

1. [PSR-4](https://www.php-fig.org/psr/psr-4/) loader is now the default. If you want to use class maps instead, use `-gen php:classmap`.
Expand Down
25 changes: 24 additions & 1 deletion lib/php/lib/Base/TBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,31 @@ private function writeList(array $var, array $spec, TProtocol $output, bool $set
} else {
$xfer += $output->writeListBegin($etype, count($var));
}
$setUsesValues = false;
if ($set && array_is_list($var)) {
// Preserve the legacy `element => true` marker form when every
// value is `true`. This keeps ambiguous `set<bool>` inputs such
// as `[true]` on the backward-compatible path.

foreach ($var as $candidate) {
if ($candidate !== true) {
$setUsesValues = true;
break;
}
}
}
foreach ($var as $key => $val) {
$elem = $set ? $key : $val;
$elem = $set && !$setUsesValues ? $key : $val;
Comment thread
sveneld marked this conversation as resolved.
if ($set) {
// Match the generated serializer for both keyed sets and value lists.
$elem = match ($etype) {
TType::BOOL => (bool) $elem,
TType::BYTE, TType::I16, TType::I32, TType::I64 => (int) $elem,
TType::DOUBLE => (float) $elem,
TType::STRING, TType::UUID => (string) $elem,
default => $elem,
};
}
if (isset($ewrite)) {
$xfer += $output->$ewrite($elem);
} else {
Expand Down
25 changes: 24 additions & 1 deletion lib/php/lib/Exception/TException.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,31 @@ private function writeList(array $var, array $spec, TProtocol $output, bool $set
} else {
$xfer += $output->writeListBegin($etype, count($var));
}
$setUsesValues = false;
if ($set && array_is_list($var)) {
// Preserve the legacy `element => true` marker form when every
// value is `true`. This keeps ambiguous `set<bool>` inputs such
// as `[true]` on the backward-compatible path.

foreach ($var as $candidate) {
if ($candidate !== true) {
$setUsesValues = true;
break;
}
}
}
foreach ($var as $key => $val) {
$elem = $set ? $key : $val;
$elem = $set && !$setUsesValues ? $key : $val;
Comment thread
sveneld marked this conversation as resolved.
if ($set) {
// Match the generated serializer for both keyed sets and value lists.
$elem = match ($etype) {
TType::BOOL => (bool) $elem,
TType::BYTE, TType::I16, TType::I32, TType::I64 => (int) $elem,
TType::DOUBLE => (float) $elem,
TType::STRING, TType::UUID => (string) $elem,
default => $elem,
};
}
if (isset($ewrite)) {
$xfer += $output->$ewrite($elem);
} else {
Expand Down
13 changes: 12 additions & 1 deletion lib/php/src/ext/thrift_protocol/php_thrift_protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,18 @@ void binary_serialize(int8_t thrift_typeID, PHPOutputTransport& transport, zval*

transport.writeI32(zend_hash_num_elements(ht));
HashPosition key_ptr;
if(ttype_is_scalar(keytype)){
bool set_uses_values = false;
if (ttype_is_scalar(keytype) && zend_array_is_list(ht)) {
// All-true lists retain the legacy element => true interpretation.
ZEND_HASH_FOREACH_VAL(ht, val_ptr) {
ZVAL_DEREF(val_ptr);
if (Z_TYPE_P(val_ptr) != IS_TRUE) {
set_uses_values = true;
break;
}
} ZEND_HASH_FOREACH_END();
}
if(ttype_is_scalar(keytype) && !set_uses_values){
for (zend_hash_internal_pointer_reset_ex(ht, &key_ptr);
(val_ptr = zend_hash_get_current_data_ex(ht, &key_ptr)) != nullptr;
zend_hash_move_forward_ex(ht, &key_ptr)) {
Expand Down
94 changes: 94 additions & 0 deletions lib/php/src/ext/thrift_protocol/tests/scalar_sets.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
--TEST--
Scalar sets accept value lists and preserve legacy keyed sets
--SKIPIF--
<?php
if (!extension_loaded('thrift_protocol')) {
echo 'skip thrift_protocol extension not loaded';
}
?>
--FILE--
<?php
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TMemoryBuffer;
use Thrift\Type\TType;

spl_autoload_register(function ($class) {
if (strpos($class, 'Thrift\\') === 0) {
require __DIR__ . '/../../../../lib/'
. str_replace('\\', '/', substr($class, 7)) . '.php';
}
});

class ScalarSetPayload
{
public static $tspec;
public static $isValidate = false;
public $items;
}

$cases = [
'empty' => [TType::I32, [], []],
'integer list' => [TType::I32, [42, -7, 19], [42, -7, 19]],
'legacy sequential keys' => [TType::I32, [0 => true, 1 => true], [0, 1]],
'string list' => [TType::STRING, ['a', '123'], ['a', '123']],
'legacy numeric string' => [TType::STRING, ['123' => true], ['123']],
'bool list' => [TType::BOOL, [true, false], [true, false]],
'ambiguous bool' => [TType::BOOL, [true], [false]],
'legacy bool' => [TType::BOOL, [1 => true], [true]],
];
foreach ($cases as $name => [$type, $input, $elements]) {
ScalarSetPayload::$tspec = [1 => [
'var' => 'items', 'type' => TType::SET,
'etype' => $type, 'elem' => ['type' => $type],
]];
$payload = new ScalarSetPayload();
$payload->items = $input;
$actual = new TMemoryBuffer();
thrift_protocol_write_binary(new TBinaryProtocol($actual), 'test', 1, $payload, 0, true);

$expected = new TMemoryBuffer();
$protocol = new TBinaryProtocol($expected);
$protocol->writeMessageBegin('test', 1, 0);
$protocol->writeStructBegin('ScalarSetPayload');
$protocol->writeFieldBegin('items', TType::SET, 1);
$protocol->writeSetBegin($type, count($elements));
$writer = [TType::I32 => 'writeI32', TType::STRING => 'writeString', TType::BOOL => 'writeBool'][$type];
foreach ($elements as $element) {
$protocol->$writer($element);
}
$protocol->writeSetEnd();
$protocol->writeFieldEnd();
$protocol->writeFieldStop();
$protocol->writeStructEnd();
$protocol->writeMessageEnd();
echo $name, ': ', $actual->getBuffer() === $expected->getBuffer() ? 'OK' : 'FAIL', "\n";
}
?>
--EXPECT--
empty: OK
integer list: OK
legacy sequential keys: OK
string list: OK
legacy numeric string: OK
bool list: OK
ambiguous bool: OK
legacy bool: OK
111 changes: 111 additions & 0 deletions lib/php/test/Integration/Lib/Protocol/ScalarSetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

declare(strict_types=1);

namespace Test\Thrift\Integration\Lib\Protocol;

use Classmap\ThriftTest\ThriftTestIf;
use Classmap\ThriftTest\ThriftTestRest;
use Classmap\ThriftTest\ThriftTest_testSet_args;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Thrift\ClassLoader\ThriftClassLoader;
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TMemoryBuffer;

class ScalarSetTest extends TestCase
{
#[DataProvider('scalarSetCoercionProvider')]
public function testScalarSetCoercionIsConsistent(
string $class,
array $fields,
array $values,
array $expected,
bool $inlined
): void {
$value = new $class(array_fill_keys($fields, $values));
$restored = new $class();
if ($inlined) {
$buffer = '';
$value->write($buffer);
$restored->read(new TMemoryBuffer($buffer));
} else {
$protocol = new TBinaryProtocol(new TMemoryBuffer());
$value->write($protocol);
$restored->read($protocol);
}

foreach ($fields as $field) {
$this->assertSame($expected, $restored->$field);
}
}

public static function scalarSetCoercionProvider(): iterable
{
foreach (['Basic', 'BasicInline', 'ValidateOop'] as $namespace) {
$inlined = $namespace === 'BasicInline';
yield $namespace . ' numeric strings' => [
$namespace . '\\ThriftTest\\ThriftTest_testSet_args',
['thing'], ['10', '20'], [10 => true, 20 => true], $inlined,
];
yield $namespace . ' integer booleans' => [
$namespace . '\\TestValidators\\BoolSetTest',
['direct', 'aliased', 'chained'], [1, 0], [1 => true, 0 => true], $inlined,
];
}
}

#[DataProvider('restSetProvider')]
public function testRestSetPreservesElementsThroughSerialization(array $elements): void
{
$loader = new ThriftClassLoader();
$loader->registerDefinition('Classmap', __DIR__ . '/../../../Resources/packages/phpcm');
$loader->register();

try {
$expected = array_fill_keys($elements, true);
$handler = $this->createMock(ThriftTestIf::class);
$handler->expects($this->once())->method('testSet')
->willReturnCallback(function (array $values): array {
$protocol = new TBinaryProtocol(new TMemoryBuffer());
$args = new ThriftTest_testSet_args(['thing' => $values]);
$args->write($protocol);
$restored = new ThriftTest_testSet_args();
$restored->read($protocol);
return $restored->thing;
});

$rest = new ThriftTestRest($handler);
$this->assertSame($expected, $rest->testSet(['thing' => json_encode($elements)]));
} finally {
spl_autoload_unregister([$loader, 'loadClass']);
}
}

public static function restSetProvider(): iterable
{
yield 'empty' => [[]];
yield 'zero and one' => [[0, 1]];
yield 'zero through two' => [[0, 1, 2]];
yield 'nonsequential' => [[-5, 10]];
}
}
Loading
Loading